Merge branch 'main' into project-panel-context-menu
This commit is contained in:
commit
dbfc7d3555
64 changed files with 4038 additions and 2696 deletions
50
Cargo.lock
generated
50
Cargo.lock
generated
|
@ -9471,6 +9471,27 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "theme_selector2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"editor2",
|
||||||
|
"feature_flags2",
|
||||||
|
"fs2",
|
||||||
|
"fuzzy2",
|
||||||
|
"gpui2",
|
||||||
|
"log",
|
||||||
|
"parking_lot 0.11.2",
|
||||||
|
"picker2",
|
||||||
|
"postage",
|
||||||
|
"settings2",
|
||||||
|
"smol",
|
||||||
|
"theme2",
|
||||||
|
"ui2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "thiserror"
|
name = "thiserror"
|
||||||
version = "1.0.48"
|
version = "1.0.48"
|
||||||
|
@ -11054,6 +11075,31 @@ dependencies = [
|
||||||
"workspace",
|
"workspace",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "welcome2"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"anyhow",
|
||||||
|
"client2",
|
||||||
|
"db2",
|
||||||
|
"editor2",
|
||||||
|
"fs2",
|
||||||
|
"fuzzy2",
|
||||||
|
"gpui2",
|
||||||
|
"install_cli2",
|
||||||
|
"log",
|
||||||
|
"picker2",
|
||||||
|
"project2",
|
||||||
|
"schemars",
|
||||||
|
"serde",
|
||||||
|
"settings2",
|
||||||
|
"theme2",
|
||||||
|
"theme_selector2",
|
||||||
|
"ui2",
|
||||||
|
"util",
|
||||||
|
"workspace2",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "which"
|
name = "which"
|
||||||
version = "4.4.2"
|
version = "4.4.2"
|
||||||
|
@ -11508,7 +11554,7 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.115.0"
|
version = "0.116.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"activity_indicator",
|
"activity_indicator",
|
||||||
"ai",
|
"ai",
|
||||||
|
@ -11720,6 +11766,7 @@ dependencies = [
|
||||||
"terminal_view2",
|
"terminal_view2",
|
||||||
"text2",
|
"text2",
|
||||||
"theme2",
|
"theme2",
|
||||||
|
"theme_selector2",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"tiny_http",
|
"tiny_http",
|
||||||
"toml 0.5.11",
|
"toml 0.5.11",
|
||||||
|
@ -11757,6 +11804,7 @@ dependencies = [
|
||||||
"urlencoding",
|
"urlencoding",
|
||||||
"util",
|
"util",
|
||||||
"uuid 1.4.1",
|
"uuid 1.4.1",
|
||||||
|
"welcome2",
|
||||||
"workspace2",
|
"workspace2",
|
||||||
"zed_actions2",
|
"zed_actions2",
|
||||||
]
|
]
|
||||||
|
|
|
@ -107,6 +107,7 @@ members = [
|
||||||
"crates/theme2",
|
"crates/theme2",
|
||||||
"crates/theme_importer",
|
"crates/theme_importer",
|
||||||
"crates/theme_selector",
|
"crates/theme_selector",
|
||||||
|
"crates/theme_selector2",
|
||||||
"crates/ui2",
|
"crates/ui2",
|
||||||
"crates/util",
|
"crates/util",
|
||||||
"crates/semantic_index",
|
"crates/semantic_index",
|
||||||
|
@ -115,6 +116,7 @@ members = [
|
||||||
"crates/vcs_menu",
|
"crates/vcs_menu",
|
||||||
"crates/workspace2",
|
"crates/workspace2",
|
||||||
"crates/welcome",
|
"crates/welcome",
|
||||||
|
"crates/welcome2",
|
||||||
"crates/xtask",
|
"crates/xtask",
|
||||||
"crates/zed",
|
"crates/zed",
|
||||||
"crates/zed2",
|
"crates/zed2",
|
||||||
|
|
|
@ -14,8 +14,8 @@ use client::{
|
||||||
use collections::HashSet;
|
use collections::HashSet;
|
||||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
|
||||||
View, ViewContext, VisualContext, WeakModel, WeakView,
|
Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
|
||||||
};
|
};
|
||||||
pub use participant::ParticipantLocation;
|
pub use participant::ParticipantLocation;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
|
@ -334,12 +334,55 @@ impl ActiveCall {
|
||||||
pub fn join_channel(
|
pub fn join_channel(
|
||||||
&mut self,
|
&mut self,
|
||||||
channel_id: u64,
|
channel_id: u64,
|
||||||
|
requesting_window: Option<WindowHandle<Workspace>>,
|
||||||
cx: &mut ModelContext<Self>,
|
cx: &mut ModelContext<Self>,
|
||||||
) -> Task<Result<Option<Model<Room>>>> {
|
) -> Task<Result<Option<Model<Room>>>> {
|
||||||
if let Some(room) = self.room().cloned() {
|
if let Some(room) = self.room().cloned() {
|
||||||
if room.read(cx).channel_id() == Some(channel_id) {
|
if room.read(cx).channel_id() == Some(channel_id) {
|
||||||
return Task::ready(Ok(Some(room)));
|
return cx.spawn(|_, _| async move {
|
||||||
} else {
|
todo!();
|
||||||
|
// let future = room.update(&mut cx, |room, cx| {
|
||||||
|
// room.most_active_project(cx).map(|(host, project)| {
|
||||||
|
// room.join_project(project, host, app_state.clone(), cx)
|
||||||
|
// })
|
||||||
|
// })
|
||||||
|
|
||||||
|
// if let Some(future) = future {
|
||||||
|
// future.await?;
|
||||||
|
// }
|
||||||
|
|
||||||
|
// Ok(Some(room))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_prompt = room.update(cx, |room, _| {
|
||||||
|
room.channel_id().is_some()
|
||||||
|
&& room.is_sharing_project()
|
||||||
|
&& room.remote_participants().len() > 0
|
||||||
|
});
|
||||||
|
if should_prompt && requesting_window.is_some() {
|
||||||
|
return cx.spawn(|this, mut cx| async move {
|
||||||
|
let answer = requesting_window.unwrap().update(&mut cx, |_, cx| {
|
||||||
|
cx.prompt(
|
||||||
|
PromptLevel::Warning,
|
||||||
|
"Leaving this call will unshare your current project.\nDo you want to switch channels?",
|
||||||
|
&["Yes, Join Channel", "Cancel"],
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if answer.await? == 1 {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
room.update(&mut cx, |room, cx| room.clear_state(cx))?;
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.join_channel(channel_id, requesting_window, cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if room.read(cx).channel_id().is_some() {
|
||||||
room.update(cx, |room, cx| room.clear_state(cx));
|
room.update(cx, |room, cx| room.clear_state(cx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -693,8 +693,8 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||||
read_credentials_from_keychain(cx).await.is_some()
|
read_credentials_from_keychain(cx).is_some()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_recursion(?Send)]
|
#[async_recursion(?Send)]
|
||||||
|
@ -725,7 +725,7 @@ impl Client {
|
||||||
let mut read_from_keychain = false;
|
let mut read_from_keychain = false;
|
||||||
let mut credentials = self.state.read().credentials.clone();
|
let mut credentials = self.state.read().credentials.clone();
|
||||||
if credentials.is_none() && try_keychain {
|
if credentials.is_none() && try_keychain {
|
||||||
credentials = read_credentials_from_keychain(cx).await;
|
credentials = read_credentials_from_keychain(cx);
|
||||||
read_from_keychain = credentials.is_some();
|
read_from_keychain = credentials.is_some();
|
||||||
}
|
}
|
||||||
if credentials.is_none() {
|
if credentials.is_none() {
|
||||||
|
@ -1324,7 +1324,7 @@ impl Client {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
fn read_credentials_from_keychain(cx: &AsyncAppContext) -> Option<Credentials> {
|
||||||
if IMPERSONATE_LOGIN.is_some() {
|
if IMPERSONATE_LOGIN.is_some() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
|
@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member(
|
||||||
let active_call_b = cx_b.read(ActiveCall::global);
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
assert!(active_call_b
|
assert!(active_call_b
|
||||||
.update(cx_b, |active_call, cx| active_call.join_channel(sub_id, cx))
|
.update(cx_b, |active_call, cx| active_call
|
||||||
|
.join_channel(sub_id, None, cx))
|
||||||
.await
|
.await
|
||||||
.is_ok());
|
.is_ok());
|
||||||
}
|
}
|
||||||
|
@ -394,7 +395,9 @@ async fn test_channel_room(
|
||||||
let active_call_b = cx_b.read(ActiveCall::global);
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
.update(cx_a, |active_call, cx| {
|
||||||
|
active_call.join_channel(zed_id, None, cx)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -442,7 +445,9 @@ async fn test_channel_room(
|
||||||
});
|
});
|
||||||
|
|
||||||
active_call_b
|
active_call_b
|
||||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
.update(cx_b, |active_call, cx| {
|
||||||
|
active_call.join_channel(zed_id, None, cx)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -559,12 +564,16 @@ async fn test_channel_room(
|
||||||
});
|
});
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
.update(cx_a, |active_call, cx| {
|
||||||
|
active_call.join_channel(zed_id, None, cx)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
active_call_b
|
active_call_b
|
||||||
.update(cx_b, |active_call, cx| active_call.join_channel(zed_id, cx))
|
.update(cx_b, |active_call, cx| {
|
||||||
|
active_call.join_channel(zed_id, None, cx)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -608,7 +617,9 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||||
let active_call_a = cx_a.read(ActiveCall::global);
|
let active_call_a = cx_a.read(ActiveCall::global);
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |active_call, cx| active_call.join_channel(zed_id, cx))
|
.update(cx_a, |active_call, cx| {
|
||||||
|
active_call.join_channel(zed_id, None, cx)
|
||||||
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |active_call, cx| {
|
.update(cx_a, |active_call, cx| {
|
||||||
active_call.join_channel(rust_id, cx)
|
active_call.join_channel(rust_id, None, cx)
|
||||||
})
|
})
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
@ -793,7 +804,7 @@ async fn test_call_from_channel(
|
||||||
let active_call_b = cx_b.read(ActiveCall::global);
|
let active_call_b = cx_b.read(ActiveCall::global);
|
||||||
|
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| call.join_channel(channel_id, cx))
|
.update(cx_a, |call, cx| call.join_channel(channel_id, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1286,7 +1297,7 @@ async fn test_guest_access(
|
||||||
|
|
||||||
// Non-members should not be allowed to join
|
// Non-members should not be allowed to join
|
||||||
assert!(active_call_b
|
assert!(active_call_b
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
|
@ -1308,7 +1319,7 @@ async fn test_guest_access(
|
||||||
|
|
||||||
// Client B joins channel A as a guest
|
// Client B joins channel A as a guest
|
||||||
active_call_b
|
active_call_b
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_a, cx))
|
.update(cx_b, |call, cx| call.join_channel(channel_a, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1341,7 +1352,7 @@ async fn test_guest_access(
|
||||||
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
assert_channels_list_shape(client_b.channel_store(), cx_b, &[]);
|
||||||
|
|
||||||
active_call_b
|
active_call_b
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_b, cx))
|
.update(cx_b, |call, cx| call.join_channel(channel_b, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
@ -1372,7 +1383,7 @@ async fn test_invite_access(
|
||||||
|
|
||||||
// should not be allowed to join
|
// should not be allowed to join
|
||||||
assert!(active_call_b
|
assert!(active_call_b
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||||
.await
|
.await
|
||||||
.is_err());
|
.is_err());
|
||||||
|
|
||||||
|
@ -1390,7 +1401,7 @@ async fn test_invite_access(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
active_call_b
|
active_call_b
|
||||||
.update(cx_b, |call, cx| call.join_channel(channel_b_id, cx))
|
.update(cx_b, |call, cx| call.join_channel(channel_b_id, None, cx))
|
||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
|
|
|
@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||||
|
|
||||||
// Simultaneously join channel 1 and then channel 2
|
// Simultaneously join channel 1 and then channel 2
|
||||||
active_call_a
|
active_call_a
|
||||||
.update(cx_a, |call, cx| call.join_channel(channel_1, cx))
|
.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx))
|
||||||
.detach();
|
.detach();
|
||||||
let join_channel_2 = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, cx));
|
let join_channel_2 =
|
||||||
|
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_2, None, cx));
|
||||||
|
|
||||||
join_channel_2.await.unwrap();
|
join_channel_2.await.unwrap();
|
||||||
|
|
||||||
|
@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||||
call.invite(client_c.user_id().unwrap(), None, cx)
|
call.invite(client_c.user_id().unwrap(), None, cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
let join_channel =
|
||||||
|
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||||
|
|
||||||
b_invite.await.unwrap();
|
b_invite.await.unwrap();
|
||||||
c_invite.await.unwrap();
|
c_invite.await.unwrap();
|
||||||
|
@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
// Simultaneously join channel 1 and call user B and user C from client A.
|
// Simultaneously join channel 1 and call user B and user C from client A.
|
||||||
let join_channel = active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, cx));
|
let join_channel =
|
||||||
|
active_call_a.update(cx_a, |call, cx| call.join_channel(channel_1, None, cx));
|
||||||
|
|
||||||
let b_invite = active_call_a.update(cx_a, |call, cx| {
|
let b_invite = active_call_a.update(cx_a, |call, cx| {
|
||||||
call.invite(client_b.user_id().unwrap(), None, cx)
|
call.invite(client_b.user_id().unwrap(), None, cx)
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -181,7 +181,6 @@ impl PickerDelegate for ContactFinderDelegate {
|
||||||
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
ContactRequestStatus::RequestSent => Some("icons/x.svg"),
|
||||||
ContactRequestStatus::RequestAccepted => None,
|
ContactRequestStatus::RequestAccepted => None,
|
||||||
};
|
};
|
||||||
dbg!(icon_path);
|
|
||||||
Some(
|
Some(
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
|
|
|
@ -37,7 +37,10 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
|
use ui::{
|
||||||
|
h_stack, Avatar, Button, ButtonCommon, ButtonLike, ButtonVariant, Clickable, Color, IconButton,
|
||||||
|
IconElement, IconSize, KeyBinding, Tooltip,
|
||||||
|
};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
use workspace::{notifications::NotifyResultExt, Workspace};
|
use workspace::{notifications::NotifyResultExt, Workspace};
|
||||||
|
|
||||||
|
@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
}))
|
}))
|
||||||
|
// Temporary, will be removed when the last part of button2 is merged
|
||||||
|
.child(
|
||||||
|
div().border().border_color(gpui::blue()).child(
|
||||||
|
ButtonLike::new("test-button")
|
||||||
|
.children([
|
||||||
|
Avatar::uri(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
)
|
||||||
|
.into_element()
|
||||||
|
.into_any(),
|
||||||
|
IconElement::new(ui::Icon::ChevronDown)
|
||||||
|
.size(IconSize::Small)
|
||||||
|
.into_element()
|
||||||
|
.into_any(),
|
||||||
|
])
|
||||||
|
.on_click(move |event, _cx| {
|
||||||
|
dbg!(format!("clicked: {:?}", event.down.position));
|
||||||
|
})
|
||||||
|
.tooltip(|cx| Tooltip::text("Test tooltip", cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
use std::{
|
||||||
|
cmp::{self, Reverse},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use collections::{CommandPaletteFilter, HashMap};
|
use collections::{CommandPaletteFilter, HashMap};
|
||||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
|
@ -5,10 +10,7 @@ use gpui::{
|
||||||
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::{
|
|
||||||
cmp::{self, Reverse},
|
|
||||||
sync::Arc,
|
|
||||||
};
|
|
||||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||||
use util::{
|
use util::{
|
||||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||||
|
|
|
@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> {
|
||||||
|
|
||||||
impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
|
impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
|
||||||
where
|
where
|
||||||
V: 'static + Send + Sync,
|
V: 'static,
|
||||||
{
|
{
|
||||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||||
where
|
where
|
||||||
F: Fn(bool, &mut V, &mut ViewContext<V>) + Send + Sync + 'static,
|
F: Fn(bool, &mut V, &mut ViewContext<V>) + 'static,
|
||||||
{
|
{
|
||||||
self.observe_global::<FeatureFlags>(move |v, cx| {
|
self.observe_global::<FeatureFlags>(move |v, cx| {
|
||||||
let feature_flags = cx.global::<FeatureFlags>();
|
let feature_flags = cx.global::<FeatureFlags>();
|
||||||
|
|
|
@ -162,6 +162,7 @@ macro_rules! actions {
|
||||||
|
|
||||||
( $name:ident ) => {
|
( $name:ident ) => {
|
||||||
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
|
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
|
||||||
|
#[serde(crate = "gpui::serde")]
|
||||||
pub struct $name;
|
pub struct $name;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -111,7 +111,7 @@ pub struct Component<C> {
|
||||||
|
|
||||||
pub struct CompositeElementState<C: RenderOnce> {
|
pub struct CompositeElementState<C: RenderOnce> {
|
||||||
rendered_element: Option<<C::Rendered as IntoElement>::Element>,
|
rendered_element: Option<<C::Rendered as IntoElement>::Element>,
|
||||||
rendered_element_state: <<C::Rendered as IntoElement>::Element as Element>::State,
|
rendered_element_state: Option<<<C::Rendered as IntoElement>::Element as Element>::State>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<C> Component<C> {
|
impl<C> Component<C> {
|
||||||
|
@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> {
|
||||||
cx: &mut WindowContext,
|
cx: &mut WindowContext,
|
||||||
) -> (LayoutId, Self::State) {
|
) -> (LayoutId, Self::State) {
|
||||||
let mut element = self.component.take().unwrap().render(cx).into_element();
|
let mut element = self.component.take().unwrap().render(cx).into_element();
|
||||||
let (layout_id, state) = element.layout(state.map(|s| s.rendered_element_state), cx);
|
if let Some(element_id) = element.element_id() {
|
||||||
|
let layout_id =
|
||||||
|
cx.with_element_state(element_id, |state, cx| element.layout(state, cx));
|
||||||
let state = CompositeElementState {
|
let state = CompositeElementState {
|
||||||
rendered_element: Some(element),
|
rendered_element: Some(element),
|
||||||
rendered_element_state: state,
|
rendered_element_state: None,
|
||||||
|
};
|
||||||
|
(layout_id, state)
|
||||||
|
} else {
|
||||||
|
let (layout_id, state) =
|
||||||
|
element.layout(state.and_then(|s| s.rendered_element_state), cx);
|
||||||
|
let state = CompositeElementState {
|
||||||
|
rendered_element: Some(element),
|
||||||
|
rendered_element_state: Some(state),
|
||||||
};
|
};
|
||||||
(layout_id, state)
|
(layout_id, state)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||||
state
|
let element = state.rendered_element.take().unwrap();
|
||||||
.rendered_element
|
if let Some(element_id) = element.element_id() {
|
||||||
.take()
|
cx.with_element_state(element_id, |element_state, cx| {
|
||||||
.unwrap()
|
let mut element_state = element_state.unwrap();
|
||||||
.paint(bounds, &mut state.rendered_element_state, cx);
|
element.paint(bounds, &mut element_state, cx);
|
||||||
|
((), element_state)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
element.paint(
|
||||||
|
bounds,
|
||||||
|
&mut state.rendered_element_state.as_mut().unwrap(),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -173,7 +173,7 @@ impl Element for UniformList {
|
||||||
let item_size = element_state.item_size;
|
let item_size = element_state.item_size;
|
||||||
let content_size = Size {
|
let content_size = Size {
|
||||||
width: padded_bounds.size.width,
|
width: padded_bounds.size.width,
|
||||||
height: item_size.height * self.item_count,
|
height: item_size.height * self.item_count + padding.top + padding.bottom,
|
||||||
};
|
};
|
||||||
|
|
||||||
let shared_scroll_offset = element_state
|
let shared_scroll_offset = element_state
|
||||||
|
@ -221,9 +221,7 @@ impl Element for UniformList {
|
||||||
|
|
||||||
let items = (self.render_items)(visible_range.clone(), cx);
|
let items = (self.render_items)(visible_range.clone(), cx);
|
||||||
cx.with_z_index(1, |cx| {
|
cx.with_z_index(1, |cx| {
|
||||||
let content_mask = ContentMask {
|
let content_mask = ContentMask { bounds };
|
||||||
bounds: padded_bounds,
|
|
||||||
};
|
|
||||||
cx.with_content_mask(Some(content_mask), |cx| {
|
cx.with_content_mask(Some(content_mask), |cx| {
|
||||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||||
let item_origin = padded_bounds.origin
|
let item_origin = padded_bounds.origin
|
||||||
|
|
|
@ -1939,23 +1939,6 @@ pub trait BorrowWindow: BorrowMut<Window> + BorrowMut<AppContext> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Like `with_element_state`, but for situations where the element_id is optional. If the
|
|
||||||
/// id is `None`, no state will be retrieved or stored.
|
|
||||||
fn with_optional_element_state<S, R>(
|
|
||||||
&mut self,
|
|
||||||
element_id: Option<ElementId>,
|
|
||||||
f: impl FnOnce(Option<S>, &mut Self) -> (R, S),
|
|
||||||
) -> R
|
|
||||||
where
|
|
||||||
S: 'static,
|
|
||||||
{
|
|
||||||
if let Some(element_id) = element_id {
|
|
||||||
self.with_element_state(element_id, f)
|
|
||||||
} else {
|
|
||||||
f(None, self).0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Obtain the current content mask.
|
/// Obtain the current content mask.
|
||||||
fn content_mask(&self) -> ContentMask<Pixels> {
|
fn content_mask(&self) -> ContentMask<Pixels> {
|
||||||
self.window()
|
self.window()
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use gpui::{
|
use gpui::{
|
||||||
div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
|
div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
|
||||||
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
|
||||||
|
WindowContext,
|
||||||
};
|
};
|
||||||
use std::{cmp, sync::Arc};
|
use std::{cmp, sync::Arc};
|
||||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||||
|
@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> {
|
||||||
|
|
||||||
pub trait PickerDelegate: Sized + 'static {
|
pub trait PickerDelegate: Sized + 'static {
|
||||||
type ListItem: IntoElement;
|
type ListItem: IntoElement;
|
||||||
|
|
||||||
fn match_count(&self) -> usize;
|
fn match_count(&self) -> usize;
|
||||||
fn selected_index(&self) -> usize;
|
fn selected_index(&self) -> usize;
|
||||||
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
|
fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>);
|
||||||
|
@ -205,7 +205,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
.when(self.delegate.match_count() > 0, |el| {
|
.when(self.delegate.match_count() > 0, |el| {
|
||||||
el.child(
|
el.child(
|
||||||
v_stack()
|
v_stack()
|
||||||
.p_1()
|
|
||||||
.grow()
|
.grow()
|
||||||
.child(
|
.child(
|
||||||
uniform_list(
|
uniform_list(
|
||||||
|
@ -239,7 +238,8 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.track_scroll(self.scroll_handle.clone()),
|
.track_scroll(self.scroll_handle.clone())
|
||||||
|
.p_1()
|
||||||
)
|
)
|
||||||
.max_h_72()
|
.max_h_72()
|
||||||
.overflow_hidden(),
|
.overflow_hidden(),
|
||||||
|
@ -256,3 +256,22 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn simple_picker_match(
|
||||||
|
selected: bool,
|
||||||
|
cx: &mut WindowContext,
|
||||||
|
children: impl FnOnce(&mut WindowContext) -> AnyElement,
|
||||||
|
) -> AnyElement {
|
||||||
|
let colors = cx.theme().colors();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.px_1()
|
||||||
|
.text_color(colors.text)
|
||||||
|
.text_ui()
|
||||||
|
.bg(colors.ghost_element_background)
|
||||||
|
.rounded_md()
|
||||||
|
.when(selected, |this| this.bg(colors.ghost_element_selected))
|
||||||
|
.hover(|this| this.bg(colors.ghost_element_hover))
|
||||||
|
.child((children)(cx))
|
||||||
|
.into_any()
|
||||||
|
}
|
||||||
|
|
|
@ -13,12 +13,14 @@ use node_runtime::NodeRuntime;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum Prettier {
|
pub enum Prettier {
|
||||||
Real(RealPrettier),
|
Real(RealPrettier),
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
Test(TestPrettier),
|
Test(TestPrettier),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct RealPrettier {
|
pub struct RealPrettier {
|
||||||
default: bool,
|
default: bool,
|
||||||
prettier_dir: PathBuf,
|
prettier_dir: PathBuf,
|
||||||
|
@ -26,11 +28,13 @@ pub struct RealPrettier {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TestPrettier {
|
pub struct TestPrettier {
|
||||||
prettier_dir: PathBuf,
|
prettier_dir: PathBuf,
|
||||||
default: bool,
|
default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const FAIL_THRESHOLD: usize = 4;
|
||||||
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||||
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||||
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||||
|
|
|
@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
|
||||||
const { method, id, params } = message;
|
const { method, id, params } = message;
|
||||||
if (method === undefined) {
|
if (method === undefined) {
|
||||||
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||||
|
} else if (method == "initialized") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,12 +13,14 @@ use std::{
|
||||||
};
|
};
|
||||||
use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub enum Prettier {
|
pub enum Prettier {
|
||||||
Real(RealPrettier),
|
Real(RealPrettier),
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
Test(TestPrettier),
|
Test(TestPrettier),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct RealPrettier {
|
pub struct RealPrettier {
|
||||||
default: bool,
|
default: bool,
|
||||||
prettier_dir: PathBuf,
|
prettier_dir: PathBuf,
|
||||||
|
@ -26,11 +28,13 @@ pub struct RealPrettier {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
#[derive(Clone)]
|
||||||
pub struct TestPrettier {
|
pub struct TestPrettier {
|
||||||
prettier_dir: PathBuf,
|
prettier_dir: PathBuf,
|
||||||
default: bool,
|
default: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub const FAIL_THRESHOLD: usize = 4;
|
||||||
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||||
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||||
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||||
|
|
|
@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
|
||||||
const { method, id, params } = message;
|
const { method, id, params } = message;
|
||||||
if (method === undefined) {
|
if (method === undefined) {
|
||||||
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||||
|
} else if (method == "initialized") {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (id === undefined) {
|
if (id === undefined) {
|
||||||
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
||||||
}
|
}
|
||||||
|
|
758
crates/project/src/prettier_support.rs
Normal file
758
crates/project/src/prettier_support.rs
Normal file
|
@ -0,0 +1,758 @@
|
||||||
|
use std::{
|
||||||
|
ops::ControlFlow,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use collections::HashSet;
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::{
|
||||||
|
future::{self, Shared},
|
||||||
|
FutureExt,
|
||||||
|
};
|
||||||
|
use gpui::{AsyncAppContext, ModelContext, ModelHandle, Task};
|
||||||
|
use language::{
|
||||||
|
language_settings::{Formatter, LanguageSettings},
|
||||||
|
Buffer, Language, LanguageServerName, LocalFile,
|
||||||
|
};
|
||||||
|
use lsp::LanguageServerId;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use prettier::Prettier;
|
||||||
|
use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn prettier_plugins_for_language(
|
||||||
|
language: &Language,
|
||||||
|
language_settings: &LanguageSettings,
|
||||||
|
) -> Option<HashSet<&'static str>> {
|
||||||
|
match &language_settings.formatter {
|
||||||
|
Formatter::Prettier { .. } | Formatter::Auto => {}
|
||||||
|
Formatter::LanguageServer | Formatter::External { .. } => return None,
|
||||||
|
};
|
||||||
|
let mut prettier_plugins = None;
|
||||||
|
if language.prettier_parser_name().is_some() {
|
||||||
|
prettier_plugins
|
||||||
|
.get_or_insert_with(|| HashSet::default())
|
||||||
|
.extend(
|
||||||
|
language
|
||||||
|
.lsp_adapters()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|adapter| adapter.prettier_plugins()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prettier_plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn format_with_prettier(
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Option<FormatOperation> {
|
||||||
|
if let Some((prettier_path, prettier_task)) = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.prettier_instance_for_buffer(buffer, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
match prettier_task.await {
|
||||||
|
Ok(prettier) => {
|
||||||
|
let buffer_path = buffer.update(cx, |buffer, cx| {
|
||||||
|
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||||
|
});
|
||||||
|
match prettier.format(buffer, buffer_path, cx).await {
|
||||||
|
Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => project.update(cx, |project, _| {
|
||||||
|
let instance_to_update = match prettier_path {
|
||||||
|
Some(prettier_path) => {
|
||||||
|
log::error!(
|
||||||
|
"Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
|
||||||
|
);
|
||||||
|
project.prettier_instances.get_mut(&prettier_path)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::error!("Default prettier instance failed to spawn: {e:#}");
|
||||||
|
match &mut project.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled { .. } => None,
|
||||||
|
PrettierInstallation::Installed(instance) => Some(instance),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(instance) = instance_to_update {
|
||||||
|
instance.attempt += 1;
|
||||||
|
instance.prettier = None;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultPrettier {
|
||||||
|
prettier: PrettierInstallation,
|
||||||
|
installed_plugins: HashSet<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PrettierInstallation {
|
||||||
|
NotInstalled {
|
||||||
|
attempts: usize,
|
||||||
|
installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
|
||||||
|
not_installed_plugins: HashSet<&'static str>,
|
||||||
|
},
|
||||||
|
Installed(PrettierInstance),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PrettierInstance {
|
||||||
|
attempt: usize,
|
||||||
|
prettier: Option<PrettierTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DefaultPrettier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
prettier: PrettierInstallation::NotInstalled {
|
||||||
|
attempts: 0,
|
||||||
|
installation_task: None,
|
||||||
|
not_installed_plugins: HashSet::default(),
|
||||||
|
},
|
||||||
|
installed_plugins: HashSet::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultPrettier {
|
||||||
|
pub fn instance(&self) -> Option<&PrettierInstance> {
|
||||||
|
if let PrettierInstallation::Installed(instance) = &self.prettier {
|
||||||
|
Some(instance)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prettier_task(
|
||||||
|
&mut self,
|
||||||
|
node: &Arc<dyn NodeRuntime>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Option<Task<anyhow::Result<PrettierTask>>> {
|
||||||
|
match &mut self.prettier {
|
||||||
|
PrettierInstallation::NotInstalled { .. } => {
|
||||||
|
Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
|
||||||
|
}
|
||||||
|
PrettierInstallation::Installed(existing_instance) => {
|
||||||
|
existing_instance.prettier_task(node, None, worktree_id, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrettierInstance {
|
||||||
|
pub fn prettier_task(
|
||||||
|
&mut self,
|
||||||
|
node: &Arc<dyn NodeRuntime>,
|
||||||
|
prettier_dir: Option<&Path>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Option<Task<anyhow::Result<PrettierTask>>> {
|
||||||
|
if self.attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
match prettier_dir {
|
||||||
|
Some(prettier_dir) => log::warn!(
|
||||||
|
"Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
|
||||||
|
),
|
||||||
|
None => log::warn!("Default prettier exceeded launch threshold, not starting"),
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(match &self.prettier {
|
||||||
|
Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
|
||||||
|
None => match prettier_dir {
|
||||||
|
Some(prettier_dir) => {
|
||||||
|
let new_task = start_prettier(
|
||||||
|
Arc::clone(node),
|
||||||
|
prettier_dir.to_path_buf(),
|
||||||
|
worktree_id,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
self.attempt += 1;
|
||||||
|
self.prettier = Some(new_task.clone());
|
||||||
|
Task::ready(Ok(new_task))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.attempt += 1;
|
||||||
|
let node = Arc::clone(node);
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
project
|
||||||
|
.update(&mut cx, |_, cx| {
|
||||||
|
start_default_prettier(node, worktree_id, cx)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_default_prettier(
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Task<anyhow::Result<PrettierTask>> {
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
loop {
|
||||||
|
let installation_task = project.update(&mut cx, |project, _| {
|
||||||
|
match &project.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled {
|
||||||
|
installation_task, ..
|
||||||
|
} => ControlFlow::Continue(installation_task.clone()),
|
||||||
|
PrettierInstallation::Installed(default_prettier) => {
|
||||||
|
ControlFlow::Break(default_prettier.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match installation_task {
|
||||||
|
ControlFlow::Continue(None) => {
|
||||||
|
anyhow::bail!("Default prettier is not installed and cannot be started")
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(Some(installation_task)) => {
|
||||||
|
log::info!("Waiting for default prettier to install");
|
||||||
|
if let Err(e) = installation_task.await {
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled {
|
||||||
|
installation_task,
|
||||||
|
attempts,
|
||||||
|
..
|
||||||
|
} = &mut project.default_prettier.prettier
|
||||||
|
{
|
||||||
|
*installation_task = None;
|
||||||
|
*attempts += 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
anyhow::bail!(
|
||||||
|
"Cannot start default prettier due to its installation failure: {e:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let new_default_prettier = project.update(&mut cx, |project, cx| {
|
||||||
|
let new_default_prettier =
|
||||||
|
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: Some(new_default_prettier.clone()),
|
||||||
|
});
|
||||||
|
new_default_prettier
|
||||||
|
});
|
||||||
|
return Ok(new_default_prettier);
|
||||||
|
}
|
||||||
|
ControlFlow::Break(instance) => match instance.prettier {
|
||||||
|
Some(instance) => return Ok(instance),
|
||||||
|
None => {
|
||||||
|
let new_default_prettier = project.update(&mut cx, |project, cx| {
|
||||||
|
let new_default_prettier =
|
||||||
|
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: instance.attempt + 1,
|
||||||
|
prettier: Some(new_default_prettier.clone()),
|
||||||
|
});
|
||||||
|
new_default_prettier
|
||||||
|
});
|
||||||
|
return Ok(new_default_prettier);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_prettier(
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> PrettierTask {
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
log::info!("Starting prettier at path {prettier_dir:?}");
|
||||||
|
let new_server_id = project.update(&mut cx, |project, _| {
|
||||||
|
project.languages.next_language_server_id()
|
||||||
|
});
|
||||||
|
|
||||||
|
let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
|
||||||
|
.await
|
||||||
|
.context("default prettier spawn")
|
||||||
|
.map(Arc::new)
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
|
||||||
|
Ok(new_prettier)
|
||||||
|
})
|
||||||
|
.shared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_new_prettier(
|
||||||
|
project: &ModelHandle<Project>,
|
||||||
|
prettier: &Prettier,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
new_server_id: LanguageServerId,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) {
|
||||||
|
let prettier_dir = prettier.prettier_dir();
|
||||||
|
let is_default = prettier.is_default();
|
||||||
|
if is_default {
|
||||||
|
log::info!("Started default prettier in {prettier_dir:?}");
|
||||||
|
} else {
|
||||||
|
log::info!("Started prettier in {prettier_dir:?}");
|
||||||
|
}
|
||||||
|
if let Some(prettier_server) = prettier.server() {
|
||||||
|
project.update(cx, |project, cx| {
|
||||||
|
let name = if is_default {
|
||||||
|
LanguageServerName(Arc::from("prettier (default)"))
|
||||||
|
} else {
|
||||||
|
let worktree_path = worktree_id
|
||||||
|
.and_then(|id| project.worktree_for_id(id, cx))
|
||||||
|
.map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
|
||||||
|
let name = match worktree_path {
|
||||||
|
Some(worktree_path) => {
|
||||||
|
if prettier_dir == worktree_path.as_ref() {
|
||||||
|
let name = prettier_dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("prettier ({name})")
|
||||||
|
} else {
|
||||||
|
let dir_to_display = prettier_dir
|
||||||
|
.strip_prefix(worktree_path.as_ref())
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(prettier_dir);
|
||||||
|
format!("prettier ({})", dir_to_display.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => format!("prettier ({})", prettier_dir.display()),
|
||||||
|
};
|
||||||
|
LanguageServerName(Arc::from(name))
|
||||||
|
};
|
||||||
|
project
|
||||||
|
.supplementary_language_servers
|
||||||
|
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
||||||
|
cx.emit(Event::LanguageServerAdded(new_server_id));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_prettier_packages(
|
||||||
|
plugins_to_install: HashSet<&'static str>,
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let packages_to_versions =
|
||||||
|
future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
|
||||||
|
|package_name| async {
|
||||||
|
let returned_package_name = package_name.to_string();
|
||||||
|
let latest_version = node
|
||||||
|
.npm_package_latest_version(package_name)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetching latest npm version for package {returned_package_name}")
|
||||||
|
})?;
|
||||||
|
anyhow::Ok((returned_package_name, latest_version))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("fetching latest npm versions")?;
|
||||||
|
|
||||||
|
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
||||||
|
let borrowed_packages = packages_to_versions
|
||||||
|
.iter()
|
||||||
|
.map(|(package, version)| (package.as_str(), version.as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
|
||||||
|
.await
|
||||||
|
.context("fetching formatter packages")?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
|
||||||
|
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
||||||
|
fs.save(
|
||||||
|
&prettier_wrapper_path,
|
||||||
|
&text::Rope::from(prettier::PRETTIER_SERVER_JS),
|
||||||
|
text::LineEnding::Unix,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"writing {} file at {prettier_wrapper_path:?}",
|
||||||
|
prettier::PRETTIER_SERVER_FILE
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
pub fn update_prettier_settings(
|
||||||
|
&self,
|
||||||
|
worktree: &ModelHandle<Worktree>,
|
||||||
|
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) {
|
||||||
|
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
||||||
|
.iter()
|
||||||
|
.map(Path::new)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let prettier_config_file_changed = changes
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
||||||
|
.filter(|(path, _, _)| {
|
||||||
|
!path
|
||||||
|
.components()
|
||||||
|
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
||||||
|
})
|
||||||
|
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
||||||
|
let current_worktree_id = worktree.read(cx).id();
|
||||||
|
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
||||||
|
log::info!(
|
||||||
|
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
||||||
|
);
|
||||||
|
let prettiers_to_reload =
|
||||||
|
self.prettiers_per_worktree
|
||||||
|
.get(¤t_worktree_id)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|prettier_paths| prettier_paths.iter())
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|prettier_path| {
|
||||||
|
Some((
|
||||||
|
current_worktree_id,
|
||||||
|
Some(prettier_path.clone()),
|
||||||
|
self.prettier_instances.get(prettier_path)?.clone(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.chain(self.default_prettier.instance().map(|default_prettier| {
|
||||||
|
(current_worktree_id, None, default_prettier.clone())
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move {
|
||||||
|
let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
|
||||||
|
async move {
|
||||||
|
if let Some(instance) = prettier_instance.prettier {
|
||||||
|
match instance.await {
|
||||||
|
Ok(prettier) => {
|
||||||
|
prettier.clear_cache().log_err().await;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
match prettier_path {
|
||||||
|
Some(prettier_path) => log::error!(
|
||||||
|
"Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
|
||||||
|
),
|
||||||
|
None => log::error!(
|
||||||
|
"Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prettier_instance_for_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: &ModelHandle<Buffer>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let buffer_file = buffer.file();
|
||||||
|
let Some(buffer_language) = buffer.language() else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
if buffer_language.prettier_parser_name().is_none() {
|
||||||
|
return Task::ready(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_local() {
|
||||||
|
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
|
||||||
|
{
|
||||||
|
Some((worktree_id, buffer_path)) => {
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
||||||
|
return cx.spawn(|project, mut cx| async move {
|
||||||
|
match cx
|
||||||
|
.background()
|
||||||
|
.spawn(async move {
|
||||||
|
Prettier::locate_prettier_installation(
|
||||||
|
fs.as_ref(),
|
||||||
|
&installed_prettiers,
|
||||||
|
&buffer_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ControlFlow::Break(())) => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(None)) => {
|
||||||
|
let default_instance = project.update(&mut cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.prettiers_per_worktree
|
||||||
|
.entry(worktree_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(None);
|
||||||
|
project.default_prettier.prettier_task(
|
||||||
|
&node,
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
Some((None, default_instance?.log_err().await?))
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(Some(prettier_dir))) => {
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
project
|
||||||
|
.prettiers_per_worktree
|
||||||
|
.entry(worktree_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(Some(prettier_dir.clone()))
|
||||||
|
});
|
||||||
|
if let Some(prettier_task) =
|
||||||
|
project.update(&mut cx, |project, cx| {
|
||||||
|
project.prettier_instances.get_mut(&prettier_dir).map(
|
||||||
|
|existing_instance| {
|
||||||
|
existing_instance.prettier_task(
|
||||||
|
&node,
|
||||||
|
Some(&prettier_dir),
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"Found already started prettier in {prettier_dir:?}"
|
||||||
|
);
|
||||||
|
return Some((
|
||||||
|
Some(prettier_dir),
|
||||||
|
prettier_task?.await.log_err()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
||||||
|
let new_prettier_task = project.update(&mut cx, |project, cx| {
|
||||||
|
let new_prettier_task = start_prettier(
|
||||||
|
node,
|
||||||
|
prettier_dir.clone(),
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
project.prettier_instances.insert(
|
||||||
|
prettier_dir.clone(),
|
||||||
|
PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: Some(new_prettier_task.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
new_prettier_task
|
||||||
|
});
|
||||||
|
Some((Some(prettier_dir), new_prettier_task))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to determine prettier path for buffer: {e:#}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let new_task = self.default_prettier.prettier_task(&node, None, cx);
|
||||||
|
return cx
|
||||||
|
.spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Task::ready(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn install_default_prettier(
|
||||||
|
&mut self,
|
||||||
|
_worktree: Option<WorktreeId>,
|
||||||
|
plugins: HashSet<&'static str>,
|
||||||
|
_cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
// suppress unused code warnings
|
||||||
|
let _ = install_prettier_packages;
|
||||||
|
let _ = save_prettier_server_file;
|
||||||
|
|
||||||
|
self.default_prettier.installed_plugins.extend(plugins);
|
||||||
|
self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
pub fn install_default_prettier(
|
||||||
|
&mut self,
|
||||||
|
worktree: Option<WorktreeId>,
|
||||||
|
mut new_plugins: HashSet<&'static str>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(node) = self.node.as_ref().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log::info!("Initializing default prettier with plugins {new_plugins:?}");
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
|
||||||
|
self.worktree_for_id(worktree_id, cx)
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path())
|
||||||
|
}) {
|
||||||
|
Some(locate_from) => {
|
||||||
|
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
||||||
|
cx.background().spawn(async move {
|
||||||
|
Prettier::locate_prettier_installation(
|
||||||
|
fs.as_ref(),
|
||||||
|
&installed_prettiers,
|
||||||
|
locate_from.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Task::ready(Ok(ControlFlow::Continue(None))),
|
||||||
|
};
|
||||||
|
new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
|
||||||
|
let mut installation_attempt = 0;
|
||||||
|
let previous_installation_task = match &mut self.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled {
|
||||||
|
installation_task,
|
||||||
|
attempts,
|
||||||
|
not_installed_plugins,
|
||||||
|
} => {
|
||||||
|
installation_attempt = *attempts;
|
||||||
|
if installation_attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
*installation_task = None;
|
||||||
|
log::warn!(
|
||||||
|
"Default prettier installation had failed {installation_attempt} times, not attempting again",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new_plugins.extend(not_installed_plugins.iter());
|
||||||
|
installation_task.clone()
|
||||||
|
}
|
||||||
|
PrettierInstallation::Installed { .. } => {
|
||||||
|
if new_plugins.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugins_to_install = new_plugins.clone();
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let new_installation_task = cx
|
||||||
|
.spawn(|project, mut cx| async move {
|
||||||
|
match locate_prettier_installation
|
||||||
|
.await
|
||||||
|
.context("locate prettier installation")
|
||||||
|
.map_err(Arc::new)?
|
||||||
|
{
|
||||||
|
ControlFlow::Break(()) => return Ok(()),
|
||||||
|
ControlFlow::Continue(prettier_path) => {
|
||||||
|
if prettier_path.is_some() {
|
||||||
|
new_plugins.clear();
|
||||||
|
}
|
||||||
|
let mut needs_install = false;
|
||||||
|
if let Some(previous_installation_task) = previous_installation_task {
|
||||||
|
if let Err(e) = previous_installation_task.await {
|
||||||
|
log::error!("Failed to install default prettier: {e:#}");
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
|
||||||
|
*attempts += 1;
|
||||||
|
new_plugins.extend(not_installed_plugins.iter());
|
||||||
|
installation_attempt = *attempts;
|
||||||
|
needs_install = true;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if installation_attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
|
||||||
|
*installation_task = None;
|
||||||
|
};
|
||||||
|
});
|
||||||
|
log::warn!(
|
||||||
|
"Default prettier installation had failed {installation_attempt} times, not attempting again",
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
new_plugins.retain(|plugin| {
|
||||||
|
!project.default_prettier.installed_plugins.contains(plugin)
|
||||||
|
});
|
||||||
|
if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
|
||||||
|
not_installed_plugins.retain(|plugin| {
|
||||||
|
!project.default_prettier.installed_plugins.contains(plugin)
|
||||||
|
});
|
||||||
|
not_installed_plugins.extend(new_plugins.iter());
|
||||||
|
}
|
||||||
|
needs_install |= !new_plugins.is_empty();
|
||||||
|
});
|
||||||
|
if needs_install {
|
||||||
|
let installed_plugins = new_plugins.clone();
|
||||||
|
cx.background()
|
||||||
|
.spawn(async move {
|
||||||
|
save_prettier_server_file(fs.as_ref()).await?;
|
||||||
|
install_prettier_packages(new_plugins, node).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("prettier & plugins install")
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
log::info!("Initialized prettier with plugins: {installed_plugins:?}");
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: None,
|
||||||
|
});
|
||||||
|
project.default_prettier
|
||||||
|
.installed_plugins
|
||||||
|
.extend(installed_plugins);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
self.default_prettier.prettier = PrettierInstallation::NotInstalled {
|
||||||
|
attempts: installation_attempt,
|
||||||
|
installation_task: Some(new_installation_task),
|
||||||
|
not_installed_plugins: plugins_to_install,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod lsp_command;
|
mod lsp_command;
|
||||||
|
mod prettier_support;
|
||||||
pub mod project_settings;
|
pub mod project_settings;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod terminals;
|
pub mod terminals;
|
||||||
|
@ -20,7 +21,7 @@ use futures::{
|
||||||
mpsc::{self, UnboundedReceiver},
|
mpsc::{self, UnboundedReceiver},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
future::{self, try_join_all, Shared},
|
future::{try_join_all, Shared},
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
|
@ -31,9 +32,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
|
||||||
},
|
|
||||||
point_to_lsp,
|
point_to_lsp,
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||||
|
@ -54,7 +53,7 @@ use lsp_command::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use prettier::Prettier;
|
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||||
use project_settings::{LspSettings, ProjectSettings};
|
use project_settings::{LspSettings, ProjectSettings};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
|
@ -72,7 +71,7 @@ use std::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
mem,
|
mem,
|
||||||
num::NonZeroU32,
|
num::NonZeroU32,
|
||||||
ops::{ControlFlow, Range},
|
ops::Range,
|
||||||
path::{self, Component, Path, PathBuf},
|
path::{self, Component, Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
str,
|
str,
|
||||||
|
@ -85,11 +84,8 @@ use std::{
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::Anchor;
|
use text::Anchor;
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer,
|
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||||
http::HttpClient,
|
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||||
merge_json_value_into,
|
|
||||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
|
||||||
post_inc, ResultExt, TryFutureExt as _,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
|
@ -168,16 +164,9 @@ pub struct Project {
|
||||||
copilot_log_subscription: Option<lsp::Subscription>,
|
copilot_log_subscription: Option<lsp::Subscription>,
|
||||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||||
node: Option<Arc<dyn NodeRuntime>>,
|
node: Option<Arc<dyn NodeRuntime>>,
|
||||||
default_prettier: Option<DefaultPrettier>,
|
default_prettier: DefaultPrettier,
|
||||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||||
prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||||
}
|
|
||||||
|
|
||||||
struct DefaultPrettier {
|
|
||||||
instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
|
||||||
installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
installed_plugins: HashSet<&'static str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DelayedDebounced {
|
struct DelayedDebounced {
|
||||||
|
@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate {
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently, formatting operations are represented differently depending on
|
||||||
|
// whether they come from a language server or an external command.
|
||||||
|
enum FormatOperation {
|
||||||
|
Lsp(Vec<(Range<Anchor>, String)>),
|
||||||
|
External(Diff),
|
||||||
|
Prettier(Diff),
|
||||||
|
}
|
||||||
|
|
||||||
impl FormatTrigger {
|
impl FormatTrigger {
|
||||||
fn from_proto(value: i32) -> FormatTrigger {
|
fn from_proto(value: i32) -> FormatTrigger {
|
||||||
match value {
|
match value {
|
||||||
|
@ -690,7 +687,7 @@ impl Project {
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||||
node: Some(node),
|
node: Some(node),
|
||||||
default_prettier: None,
|
default_prettier: DefaultPrettier::default(),
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
}
|
}
|
||||||
|
@ -791,7 +788,7 @@ impl Project {
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||||
node: None,
|
node: None,
|
||||||
default_prettier: None,
|
default_prettier: DefaultPrettier::default(),
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
};
|
};
|
||||||
|
@ -928,8 +925,19 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut prettier_plugins_by_worktree = HashMap::default();
|
||||||
for (worktree, language, settings) in language_formatters_to_check {
|
for (worktree, language, settings) in language_formatters_to_check {
|
||||||
self.install_default_formatters(worktree, &language, &settings, cx);
|
if let Some(plugins) =
|
||||||
|
prettier_support::prettier_plugins_for_language(&language, &settings)
|
||||||
|
{
|
||||||
|
prettier_plugins_by_worktree
|
||||||
|
.entry(worktree)
|
||||||
|
.or_insert_with(|| HashSet::default())
|
||||||
|
.extend(plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
|
||||||
|
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start all the newly-enabled language servers.
|
// Start all the newly-enabled language servers.
|
||||||
|
@ -2685,8 +2693,11 @@ impl Project {
|
||||||
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||||
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||||
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||||
|
if let Some(prettier_plugins) =
|
||||||
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
prettier_support::prettier_plugins_for_language(&new_language, &settings)
|
||||||
|
{
|
||||||
|
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||||
|
};
|
||||||
if let Some(file) = buffer_file {
|
if let Some(file) = buffer_file {
|
||||||
let worktree = file.worktree.clone();
|
let worktree = file.worktree.clone();
|
||||||
if let Some(tree) = worktree.read(cx).as_local() {
|
if let Some(tree) = worktree.read(cx).as_local() {
|
||||||
|
@ -4073,8 +4084,6 @@ impl Project {
|
||||||
|
|
||||||
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||||
let ensure_final_newline = settings.ensure_final_newline_on_save;
|
let ensure_final_newline = settings.ensure_final_newline_on_save;
|
||||||
let format_on_save = settings.format_on_save.clone();
|
|
||||||
let formatter = settings.formatter.clone();
|
|
||||||
let tab_size = settings.tab_size;
|
let tab_size = settings.tab_size;
|
||||||
|
|
||||||
// First, format buffer's whitespace according to the settings.
|
// First, format buffer's whitespace according to the settings.
|
||||||
|
@ -4099,18 +4108,10 @@ impl Project {
|
||||||
buffer.end_transaction(cx)
|
buffer.end_transaction(cx)
|
||||||
});
|
});
|
||||||
|
|
||||||
// Currently, formatting operations are represented differently depending on
|
|
||||||
// whether they come from a language server or an external command.
|
|
||||||
enum FormatOperation {
|
|
||||||
Lsp(Vec<(Range<Anchor>, String)>),
|
|
||||||
External(Diff),
|
|
||||||
Prettier(Diff),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply language-specific formatting using either a language server
|
// Apply language-specific formatting using either a language server
|
||||||
// or external command.
|
// or external command.
|
||||||
let mut format_operation = None;
|
let mut format_operation = None;
|
||||||
match (formatter, format_on_save) {
|
match (&settings.formatter, &settings.format_on_save) {
|
||||||
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
||||||
|
|
||||||
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
||||||
|
@ -4155,46 +4156,11 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
if let Some((prettier_path, prettier_task)) = project
|
if let Some(new_operation) =
|
||||||
.update(&mut cx, |project, cx| {
|
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||||
project.prettier_instance_for_buffer(buffer, cx)
|
|
||||||
}).await {
|
|
||||||
match prettier_task.await
|
|
||||||
{
|
|
||||||
Ok(prettier) => {
|
|
||||||
let buffer_path = buffer.update(&mut cx, |buffer, cx| {
|
|
||||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
|
||||||
});
|
|
||||||
format_operation = Some(FormatOperation::Prettier(
|
|
||||||
prettier
|
|
||||||
.format(buffer, buffer_path, &cx)
|
|
||||||
.await
|
.await
|
||||||
.context("formatting via prettier")?,
|
{
|
||||||
));
|
format_operation = Some(new_operation);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
project.prettier_instances.remove(prettier_path);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if let Some(default_prettier) = project.default_prettier.as_mut() {
|
|
||||||
default_prettier.instance = None;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some((language_server, buffer_abs_path)) =
|
} else if let Some((language_server, buffer_abs_path)) =
|
||||||
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||||
{
|
{
|
||||||
|
@ -4212,47 +4178,12 @@ impl Project {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
(Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
if let Some((prettier_path, prettier_task)) = project
|
if let Some(new_operation) =
|
||||||
.update(&mut cx, |project, cx| {
|
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||||
project.prettier_instance_for_buffer(buffer, cx)
|
|
||||||
}).await {
|
|
||||||
match prettier_task.await
|
|
||||||
{
|
|
||||||
Ok(prettier) => {
|
|
||||||
let buffer_path = buffer.update(&mut cx, |buffer, cx| {
|
|
||||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
|
||||||
});
|
|
||||||
format_operation = Some(FormatOperation::Prettier(
|
|
||||||
prettier
|
|
||||||
.format(buffer, buffer_path, &cx)
|
|
||||||
.await
|
.await
|
||||||
.context("formatting via prettier")?,
|
{
|
||||||
));
|
format_operation = Some(new_operation);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
project.prettier_instances.remove(prettier_path);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if let Some(default_prettier) = project.default_prettier.as_mut() {
|
|
||||||
default_prettier.instance = None;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
});
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6566,85 +6497,6 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_prettier_settings(
|
|
||||||
&self,
|
|
||||||
worktree: &ModelHandle<Worktree>,
|
|
||||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) {
|
|
||||||
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
|
||||||
.iter()
|
|
||||||
.map(Path::new)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
let prettier_config_file_changed = changes
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
|
||||||
.filter(|(path, _, _)| {
|
|
||||||
!path
|
|
||||||
.components()
|
|
||||||
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
|
||||||
})
|
|
||||||
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
|
||||||
let current_worktree_id = worktree.read(cx).id();
|
|
||||||
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
|
||||||
log::info!(
|
|
||||||
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
|
||||||
);
|
|
||||||
let prettiers_to_reload = self
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.get(¤t_worktree_id)
|
|
||||||
.iter()
|
|
||||||
.flat_map(|prettier_paths| prettier_paths.iter())
|
|
||||||
.flatten()
|
|
||||||
.filter_map(|prettier_path| {
|
|
||||||
Some((
|
|
||||||
current_worktree_id,
|
|
||||||
Some(prettier_path.clone()),
|
|
||||||
self.prettier_instances.get(prettier_path)?.clone(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.chain(self.default_prettier.iter().filter_map(|default_prettier| {
|
|
||||||
Some((
|
|
||||||
current_worktree_id,
|
|
||||||
None,
|
|
||||||
default_prettier.instance.clone()?,
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
cx.background()
|
|
||||||
.spawn(async move {
|
|
||||||
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
|
|
||||||
async move {
|
|
||||||
prettier_task.await?
|
|
||||||
.clear_cache()
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
match prettier_path {
|
|
||||||
Some(prettier_path) => format!(
|
|
||||||
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
|
|
||||||
),
|
|
||||||
None => format!(
|
|
||||||
"clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
.map_err(Arc::new)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Err(e) = task_result {
|
|
||||||
log::error!("Failed to clear cache for prettier: {e:#}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
|
@ -8536,446 +8388,6 @@ impl Project {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prettier_instance_for_buffer(
|
|
||||||
&mut self,
|
|
||||||
buffer: &ModelHandle<Buffer>,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Task<
|
|
||||||
Option<(
|
|
||||||
Option<PathBuf>,
|
|
||||||
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
|
|
||||||
)>,
|
|
||||||
> {
|
|
||||||
let buffer = buffer.read(cx);
|
|
||||||
let buffer_file = buffer.file();
|
|
||||||
let Some(buffer_language) = buffer.language() else {
|
|
||||||
return Task::ready(None);
|
|
||||||
};
|
|
||||||
if buffer_language.prettier_parser_name().is_none() {
|
|
||||||
return Task::ready(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_local() {
|
|
||||||
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
|
||||||
return Task::ready(None);
|
|
||||||
};
|
|
||||||
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
|
|
||||||
{
|
|
||||||
Some((worktree_id, buffer_path)) => {
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
|
||||||
return cx.spawn(|project, mut cx| async move {
|
|
||||||
match cx
|
|
||||||
.background()
|
|
||||||
.spawn(async move {
|
|
||||||
Prettier::locate_prettier_installation(
|
|
||||||
fs.as_ref(),
|
|
||||||
&installed_prettiers,
|
|
||||||
&buffer_path,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ControlFlow::Break(())) => {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Ok(ControlFlow::Continue(None)) => {
|
|
||||||
let started_default_prettier =
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.entry(worktree_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(None);
|
|
||||||
project.default_prettier.as_ref().and_then(
|
|
||||||
|default_prettier| default_prettier.instance.clone(),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
match started_default_prettier {
|
|
||||||
Some(old_task) => return Some((None, old_task)),
|
|
||||||
None => {
|
|
||||||
let new_default_prettier = project
|
|
||||||
.update(&mut cx, |_, cx| {
|
|
||||||
start_default_prettier(node, Some(worktree_id), cx)
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
return Some((None, new_default_prettier));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ControlFlow::Continue(Some(prettier_dir))) => {
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.entry(worktree_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(Some(prettier_dir.clone()))
|
|
||||||
});
|
|
||||||
if let Some(existing_prettier) =
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
project.prettier_instances.get(&prettier_dir).cloned()
|
|
||||||
})
|
|
||||||
{
|
|
||||||
log::debug!(
|
|
||||||
"Found already started prettier in {prettier_dir:?}"
|
|
||||||
);
|
|
||||||
return Some((Some(prettier_dir), existing_prettier));
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
|
||||||
let new_prettier_task = project.update(&mut cx, |project, cx| {
|
|
||||||
let new_prettier_task = start_prettier(
|
|
||||||
node,
|
|
||||||
prettier_dir.clone(),
|
|
||||||
Some(worktree_id),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
project
|
|
||||||
.prettier_instances
|
|
||||||
.insert(prettier_dir.clone(), new_prettier_task.clone());
|
|
||||||
new_prettier_task
|
|
||||||
});
|
|
||||||
Some((Some(prettier_dir), new_prettier_task))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Some((
|
|
||||||
None,
|
|
||||||
Task::ready(Err(Arc::new(
|
|
||||||
e.context("determining prettier path"),
|
|
||||||
)))
|
|
||||||
.shared(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let started_default_prettier = self
|
|
||||||
.default_prettier
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|default_prettier| default_prettier.instance.clone());
|
|
||||||
match started_default_prettier {
|
|
||||||
Some(old_task) => return Task::ready(Some((None, old_task))),
|
|
||||||
None => {
|
|
||||||
let new_task = start_default_prettier(node, None, cx);
|
|
||||||
return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if self.remote_id().is_some() {
|
|
||||||
return Task::ready(None);
|
|
||||||
} else {
|
|
||||||
Task::ready(Some((
|
|
||||||
None,
|
|
||||||
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
fn install_default_formatters(
|
|
||||||
&mut self,
|
|
||||||
_worktree: Option<WorktreeId>,
|
|
||||||
_new_language: &Language,
|
|
||||||
_language_settings: &LanguageSettings,
|
|
||||||
_cx: &mut ModelContext<Self>,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
fn install_default_formatters(
|
|
||||||
&mut self,
|
|
||||||
worktree: Option<WorktreeId>,
|
|
||||||
new_language: &Language,
|
|
||||||
language_settings: &LanguageSettings,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) {
|
|
||||||
match &language_settings.formatter {
|
|
||||||
Formatter::Prettier { .. } | Formatter::Auto => {}
|
|
||||||
Formatter::LanguageServer | Formatter::External { .. } => return,
|
|
||||||
};
|
|
||||||
let Some(node) = self.node.as_ref().cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut prettier_plugins = None;
|
|
||||||
if new_language.prettier_parser_name().is_some() {
|
|
||||||
prettier_plugins
|
|
||||||
.get_or_insert_with(|| HashSet::<&'static str>::default())
|
|
||||||
.extend(
|
|
||||||
new_language
|
|
||||||
.lsp_adapters()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|adapter| adapter.prettier_plugins()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let Some(prettier_plugins) = prettier_plugins else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
|
|
||||||
self.worktree_for_id(worktree_id, cx)
|
|
||||||
.map(|worktree| worktree.read(cx).abs_path())
|
|
||||||
}) {
|
|
||||||
Some(locate_from) => {
|
|
||||||
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
|
||||||
cx.background().spawn(async move {
|
|
||||||
Prettier::locate_prettier_installation(
|
|
||||||
fs.as_ref(),
|
|
||||||
&installed_prettiers,
|
|
||||||
locate_from.as_ref(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => Task::ready(Ok(ControlFlow::Break(()))),
|
|
||||||
};
|
|
||||||
let mut plugins_to_install = prettier_plugins;
|
|
||||||
let previous_installation_process =
|
|
||||||
if let Some(default_prettier) = &mut self.default_prettier {
|
|
||||||
plugins_to_install
|
|
||||||
.retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
|
|
||||||
if plugins_to_install.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default_prettier.installation_process.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let default_prettier = self
|
|
||||||
.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: None,
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
});
|
|
||||||
default_prettier.installation_process = Some(
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
match locate_prettier_installation
|
|
||||||
.await
|
|
||||||
.context("locate prettier installation")
|
|
||||||
.map_err(Arc::new)?
|
|
||||||
{
|
|
||||||
ControlFlow::Break(()) => return Ok(()),
|
|
||||||
ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
|
|
||||||
ControlFlow::Continue(None) => {
|
|
||||||
let mut needs_install = match previous_installation_process {
|
|
||||||
Some(previous_installation_process) => {
|
|
||||||
previous_installation_process.await.is_err()
|
|
||||||
}
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
if let Some(default_prettier) = &mut this.default_prettier {
|
|
||||||
plugins_to_install.retain(|plugin| {
|
|
||||||
!default_prettier.installed_plugins.contains(plugin)
|
|
||||||
});
|
|
||||||
needs_install |= !plugins_to_install.is_empty();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if needs_install {
|
|
||||||
let installed_plugins = plugins_to_install.clone();
|
|
||||||
cx.background()
|
|
||||||
.spawn(async move {
|
|
||||||
install_default_prettier(plugins_to_install, node, fs).await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.context("prettier & plugins install")
|
|
||||||
.map_err(Arc::new)?;
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
let default_prettier =
|
|
||||||
this.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: Some(
|
|
||||||
Task::ready(Ok(())).shared(),
|
|
||||||
),
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
});
|
|
||||||
default_prettier.instance = None;
|
|
||||||
default_prettier.installed_plugins.extend(installed_plugins);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.shared(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_default_prettier(
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
|
|
||||||
cx.spawn(|project, mut cx| async move {
|
|
||||||
loop {
|
|
||||||
let default_prettier_installing = project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.default_prettier
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|default_prettier| default_prettier.installation_process.clone())
|
|
||||||
});
|
|
||||||
match default_prettier_installing {
|
|
||||||
Some(installation_task) => {
|
|
||||||
if installation_task.await.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
project.update(&mut cx, |project, cx| {
|
|
||||||
match project
|
|
||||||
.default_prettier
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|default_prettier| default_prettier.instance.as_mut())
|
|
||||||
{
|
|
||||||
Some(default_prettier) => default_prettier.clone(),
|
|
||||||
None => {
|
|
||||||
let new_default_prettier =
|
|
||||||
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
|
||||||
project
|
|
||||||
.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: None,
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
})
|
|
||||||
.instance = Some(new_default_prettier.clone());
|
|
||||||
new_default_prettier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_prettier(
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
prettier_dir: PathBuf,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
|
|
||||||
cx.spawn(|project, mut cx| async move {
|
|
||||||
let new_server_id = project.update(&mut cx, |project, _| {
|
|
||||||
project.languages.next_language_server_id()
|
|
||||||
});
|
|
||||||
let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
|
|
||||||
.await
|
|
||||||
.context("default prettier spawn")
|
|
||||||
.map(Arc::new)
|
|
||||||
.map_err(Arc::new)?;
|
|
||||||
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
|
|
||||||
Ok(new_prettier)
|
|
||||||
})
|
|
||||||
.shared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_new_prettier(
|
|
||||||
project: &ModelHandle<Project>,
|
|
||||||
prettier: &Prettier,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
new_server_id: LanguageServerId,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) {
|
|
||||||
let prettier_dir = prettier.prettier_dir();
|
|
||||||
let is_default = prettier.is_default();
|
|
||||||
if is_default {
|
|
||||||
log::info!("Started default prettier in {prettier_dir:?}");
|
|
||||||
} else {
|
|
||||||
log::info!("Started prettier in {prettier_dir:?}");
|
|
||||||
}
|
|
||||||
if let Some(prettier_server) = prettier.server() {
|
|
||||||
project.update(cx, |project, cx| {
|
|
||||||
let name = if is_default {
|
|
||||||
LanguageServerName(Arc::from("prettier (default)"))
|
|
||||||
} else {
|
|
||||||
let worktree_path = worktree_id
|
|
||||||
.and_then(|id| project.worktree_for_id(id, cx))
|
|
||||||
.map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
|
|
||||||
let name = match worktree_path {
|
|
||||||
Some(worktree_path) => {
|
|
||||||
if prettier_dir == worktree_path.as_ref() {
|
|
||||||
let name = prettier_dir
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.unwrap_or_default();
|
|
||||||
format!("prettier ({name})")
|
|
||||||
} else {
|
|
||||||
let dir_to_display = prettier_dir
|
|
||||||
.strip_prefix(worktree_path.as_ref())
|
|
||||||
.ok()
|
|
||||||
.unwrap_or(prettier_dir);
|
|
||||||
format!("prettier ({})", dir_to_display.display())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => format!("prettier ({})", prettier_dir.display()),
|
|
||||||
};
|
|
||||||
LanguageServerName(Arc::from(name))
|
|
||||||
};
|
|
||||||
project
|
|
||||||
.supplementary_language_servers
|
|
||||||
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
|
||||||
cx.emit(Event::LanguageServerAdded(new_server_id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
async fn install_default_prettier(
|
|
||||||
plugins_to_install: HashSet<&'static str>,
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
|
||||||
// method creates parent directory if it doesn't exist
|
|
||||||
fs.save(
|
|
||||||
&prettier_wrapper_path,
|
|
||||||
&text::Rope::from(prettier::PRETTIER_SERVER_JS),
|
|
||||||
text::LineEnding::Unix,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"writing {} file at {prettier_wrapper_path:?}",
|
|
||||||
prettier::PRETTIER_SERVER_FILE
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let packages_to_versions =
|
|
||||||
future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
|
|
||||||
|package_name| async {
|
|
||||||
let returned_package_name = package_name.to_string();
|
|
||||||
let latest_version = node
|
|
||||||
.npm_package_latest_version(package_name)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!("fetching latest npm version for package {returned_package_name}")
|
|
||||||
})?;
|
|
||||||
anyhow::Ok((returned_package_name, latest_version))
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.context("fetching latest npm versions")?;
|
|
||||||
|
|
||||||
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
|
||||||
let borrowed_packages = packages_to_versions
|
|
||||||
.iter()
|
|
||||||
.map(|(package, version)| (package.as_str(), version.as_str()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
|
|
||||||
.await
|
|
||||||
.context("fetching formatter packages")?;
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_for_copilot_events(
|
fn subscribe_for_copilot_events(
|
||||||
|
|
772
crates/project2/src/prettier_support.rs
Normal file
772
crates/project2/src/prettier_support.rs
Normal file
|
@ -0,0 +1,772 @@
|
||||||
|
use std::{
|
||||||
|
ops::ControlFlow,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
|
use anyhow::Context;
|
||||||
|
use collections::HashSet;
|
||||||
|
use fs::Fs;
|
||||||
|
use futures::{
|
||||||
|
future::{self, Shared},
|
||||||
|
FutureExt,
|
||||||
|
};
|
||||||
|
use gpui::{AsyncAppContext, Model, ModelContext, Task, WeakModel};
|
||||||
|
use language::{
|
||||||
|
language_settings::{Formatter, LanguageSettings},
|
||||||
|
Buffer, Language, LanguageServerName, LocalFile,
|
||||||
|
};
|
||||||
|
use lsp::LanguageServerId;
|
||||||
|
use node_runtime::NodeRuntime;
|
||||||
|
use prettier::Prettier;
|
||||||
|
use util::{paths::DEFAULT_PRETTIER_DIR, ResultExt, TryFutureExt};
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
Event, File, FormatOperation, PathChange, Project, ProjectEntryId, Worktree, WorktreeId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn prettier_plugins_for_language(
|
||||||
|
language: &Language,
|
||||||
|
language_settings: &LanguageSettings,
|
||||||
|
) -> Option<HashSet<&'static str>> {
|
||||||
|
match &language_settings.formatter {
|
||||||
|
Formatter::Prettier { .. } | Formatter::Auto => {}
|
||||||
|
Formatter::LanguageServer | Formatter::External { .. } => return None,
|
||||||
|
};
|
||||||
|
let mut prettier_plugins = None;
|
||||||
|
if language.prettier_parser_name().is_some() {
|
||||||
|
prettier_plugins
|
||||||
|
.get_or_insert_with(|| HashSet::default())
|
||||||
|
.extend(
|
||||||
|
language
|
||||||
|
.lsp_adapters()
|
||||||
|
.iter()
|
||||||
|
.flat_map(|adapter| adapter.prettier_plugins()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
prettier_plugins
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) async fn format_with_prettier(
|
||||||
|
project: &WeakModel<Project>,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) -> Option<FormatOperation> {
|
||||||
|
if let Some((prettier_path, prettier_task)) = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.prettier_instance_for_buffer(buffer, cx)
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
match prettier_task.await {
|
||||||
|
Ok(prettier) => {
|
||||||
|
let buffer_path = buffer
|
||||||
|
.update(cx, |buffer, cx| {
|
||||||
|
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
match prettier.format(buffer, buffer_path, cx).await {
|
||||||
|
Ok(new_diff) => return Some(FormatOperation::Prettier(new_diff)),
|
||||||
|
Err(e) => {
|
||||||
|
log::error!(
|
||||||
|
"Prettier instance from {prettier_path:?} failed to format a buffer: {e:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => project
|
||||||
|
.update(cx, |project, _| {
|
||||||
|
let instance_to_update = match prettier_path {
|
||||||
|
Some(prettier_path) => {
|
||||||
|
log::error!(
|
||||||
|
"Prettier instance from path {prettier_path:?} failed to spawn: {e:#}"
|
||||||
|
);
|
||||||
|
project.prettier_instances.get_mut(&prettier_path)
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
log::error!("Default prettier instance failed to spawn: {e:#}");
|
||||||
|
match &mut project.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled { .. } => None,
|
||||||
|
PrettierInstallation::Installed(instance) => Some(instance),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(instance) = instance_to_update {
|
||||||
|
instance.attempt += 1;
|
||||||
|
instance.prettier = None;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.ok()?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DefaultPrettier {
|
||||||
|
prettier: PrettierInstallation,
|
||||||
|
installed_plugins: HashSet<&'static str>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PrettierInstallation {
|
||||||
|
NotInstalled {
|
||||||
|
attempts: usize,
|
||||||
|
installation_task: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
|
||||||
|
not_installed_plugins: HashSet<&'static str>,
|
||||||
|
},
|
||||||
|
Installed(PrettierInstance),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type PrettierTask = Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>;
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct PrettierInstance {
|
||||||
|
attempt: usize,
|
||||||
|
prettier: Option<PrettierTask>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for DefaultPrettier {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
prettier: PrettierInstallation::NotInstalled {
|
||||||
|
attempts: 0,
|
||||||
|
installation_task: None,
|
||||||
|
not_installed_plugins: HashSet::default(),
|
||||||
|
},
|
||||||
|
installed_plugins: HashSet::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DefaultPrettier {
|
||||||
|
pub fn instance(&self) -> Option<&PrettierInstance> {
|
||||||
|
if let PrettierInstallation::Installed(instance) = &self.prettier {
|
||||||
|
Some(instance)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prettier_task(
|
||||||
|
&mut self,
|
||||||
|
node: &Arc<dyn NodeRuntime>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Option<Task<anyhow::Result<PrettierTask>>> {
|
||||||
|
match &mut self.prettier {
|
||||||
|
PrettierInstallation::NotInstalled { .. } => {
|
||||||
|
Some(start_default_prettier(Arc::clone(node), worktree_id, cx))
|
||||||
|
}
|
||||||
|
PrettierInstallation::Installed(existing_instance) => {
|
||||||
|
existing_instance.prettier_task(node, None, worktree_id, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PrettierInstance {
|
||||||
|
pub fn prettier_task(
|
||||||
|
&mut self,
|
||||||
|
node: &Arc<dyn NodeRuntime>,
|
||||||
|
prettier_dir: Option<&Path>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Option<Task<anyhow::Result<PrettierTask>>> {
|
||||||
|
if self.attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
match prettier_dir {
|
||||||
|
Some(prettier_dir) => log::warn!(
|
||||||
|
"Prettier from path {prettier_dir:?} exceeded launch threshold, not starting"
|
||||||
|
),
|
||||||
|
None => log::warn!("Default prettier exceeded launch threshold, not starting"),
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(match &self.prettier {
|
||||||
|
Some(prettier_task) => Task::ready(Ok(prettier_task.clone())),
|
||||||
|
None => match prettier_dir {
|
||||||
|
Some(prettier_dir) => {
|
||||||
|
let new_task = start_prettier(
|
||||||
|
Arc::clone(node),
|
||||||
|
prettier_dir.to_path_buf(),
|
||||||
|
worktree_id,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
self.attempt += 1;
|
||||||
|
self.prettier = Some(new_task.clone());
|
||||||
|
Task::ready(Ok(new_task))
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
self.attempt += 1;
|
||||||
|
let node = Arc::clone(node);
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
project
|
||||||
|
.update(&mut cx, |_, cx| {
|
||||||
|
start_default_prettier(node, worktree_id, cx)
|
||||||
|
})?
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_default_prettier(
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> Task<anyhow::Result<PrettierTask>> {
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
loop {
|
||||||
|
let installation_task = project.update(&mut cx, |project, _| {
|
||||||
|
match &project.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled {
|
||||||
|
installation_task, ..
|
||||||
|
} => ControlFlow::Continue(installation_task.clone()),
|
||||||
|
PrettierInstallation::Installed(default_prettier) => {
|
||||||
|
ControlFlow::Break(default_prettier.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
match installation_task {
|
||||||
|
ControlFlow::Continue(None) => {
|
||||||
|
anyhow::bail!("Default prettier is not installed and cannot be started")
|
||||||
|
}
|
||||||
|
ControlFlow::Continue(Some(installation_task)) => {
|
||||||
|
log::info!("Waiting for default prettier to install");
|
||||||
|
if let Err(e) = installation_task.await {
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled {
|
||||||
|
installation_task,
|
||||||
|
attempts,
|
||||||
|
..
|
||||||
|
} = &mut project.default_prettier.prettier
|
||||||
|
{
|
||||||
|
*installation_task = None;
|
||||||
|
*attempts += 1;
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
anyhow::bail!(
|
||||||
|
"Cannot start default prettier due to its installation failure: {e:#}"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let new_default_prettier = project.update(&mut cx, |project, cx| {
|
||||||
|
let new_default_prettier =
|
||||||
|
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: Some(new_default_prettier.clone()),
|
||||||
|
});
|
||||||
|
new_default_prettier
|
||||||
|
})?;
|
||||||
|
return Ok(new_default_prettier);
|
||||||
|
}
|
||||||
|
ControlFlow::Break(instance) => match instance.prettier {
|
||||||
|
Some(instance) => return Ok(instance),
|
||||||
|
None => {
|
||||||
|
let new_default_prettier = project.update(&mut cx, |project, cx| {
|
||||||
|
let new_default_prettier =
|
||||||
|
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: instance.attempt + 1,
|
||||||
|
prettier: Some(new_default_prettier.clone()),
|
||||||
|
});
|
||||||
|
new_default_prettier
|
||||||
|
})?;
|
||||||
|
return Ok(new_default_prettier);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_prettier(
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
prettier_dir: PathBuf,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) -> PrettierTask {
|
||||||
|
cx.spawn(|project, mut cx| async move {
|
||||||
|
log::info!("Starting prettier at path {prettier_dir:?}");
|
||||||
|
let new_server_id = project.update(&mut cx, |project, _| {
|
||||||
|
project.languages.next_language_server_id()
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
|
||||||
|
.await
|
||||||
|
.context("default prettier spawn")
|
||||||
|
.map(Arc::new)
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
|
||||||
|
Ok(new_prettier)
|
||||||
|
})
|
||||||
|
.shared()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_new_prettier(
|
||||||
|
project: &WeakModel<Project>,
|
||||||
|
prettier: &Prettier,
|
||||||
|
worktree_id: Option<WorktreeId>,
|
||||||
|
new_server_id: LanguageServerId,
|
||||||
|
cx: &mut AsyncAppContext,
|
||||||
|
) {
|
||||||
|
let prettier_dir = prettier.prettier_dir();
|
||||||
|
let is_default = prettier.is_default();
|
||||||
|
if is_default {
|
||||||
|
log::info!("Started default prettier in {prettier_dir:?}");
|
||||||
|
} else {
|
||||||
|
log::info!("Started prettier in {prettier_dir:?}");
|
||||||
|
}
|
||||||
|
if let Some(prettier_server) = prettier.server() {
|
||||||
|
project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
let name = if is_default {
|
||||||
|
LanguageServerName(Arc::from("prettier (default)"))
|
||||||
|
} else {
|
||||||
|
let worktree_path = worktree_id
|
||||||
|
.and_then(|id| project.worktree_for_id(id, cx))
|
||||||
|
.map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
|
||||||
|
let name = match worktree_path {
|
||||||
|
Some(worktree_path) => {
|
||||||
|
if prettier_dir == worktree_path.as_ref() {
|
||||||
|
let name = prettier_dir
|
||||||
|
.file_name()
|
||||||
|
.and_then(|name| name.to_str())
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("prettier ({name})")
|
||||||
|
} else {
|
||||||
|
let dir_to_display = prettier_dir
|
||||||
|
.strip_prefix(worktree_path.as_ref())
|
||||||
|
.ok()
|
||||||
|
.unwrap_or(prettier_dir);
|
||||||
|
format!("prettier ({})", dir_to_display.display())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => format!("prettier ({})", prettier_dir.display()),
|
||||||
|
};
|
||||||
|
LanguageServerName(Arc::from(name))
|
||||||
|
};
|
||||||
|
project
|
||||||
|
.supplementary_language_servers
|
||||||
|
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
||||||
|
cx.emit(Event::LanguageServerAdded(new_server_id));
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn install_prettier_packages(
|
||||||
|
plugins_to_install: HashSet<&'static str>,
|
||||||
|
node: Arc<dyn NodeRuntime>,
|
||||||
|
) -> anyhow::Result<()> {
|
||||||
|
let packages_to_versions =
|
||||||
|
future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
|
||||||
|
|package_name| async {
|
||||||
|
let returned_package_name = package_name.to_string();
|
||||||
|
let latest_version = node
|
||||||
|
.npm_package_latest_version(package_name)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!("fetching latest npm version for package {returned_package_name}")
|
||||||
|
})?;
|
||||||
|
anyhow::Ok((returned_package_name, latest_version))
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.context("fetching latest npm versions")?;
|
||||||
|
|
||||||
|
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
||||||
|
let borrowed_packages = packages_to_versions
|
||||||
|
.iter()
|
||||||
|
.map(|(package, version)| (package.as_str(), version.as_str()))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
|
||||||
|
.await
|
||||||
|
.context("fetching formatter packages")?;
|
||||||
|
anyhow::Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn save_prettier_server_file(fs: &dyn Fs) -> Result<(), anyhow::Error> {
|
||||||
|
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
||||||
|
fs.save(
|
||||||
|
&prettier_wrapper_path,
|
||||||
|
&text::Rope::from(prettier::PRETTIER_SERVER_JS),
|
||||||
|
text::LineEnding::Unix,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"writing {} file at {prettier_wrapper_path:?}",
|
||||||
|
prettier::PRETTIER_SERVER_FILE
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Project {
|
||||||
|
pub fn update_prettier_settings(
|
||||||
|
&self,
|
||||||
|
worktree: &Model<Worktree>,
|
||||||
|
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
||||||
|
cx: &mut ModelContext<'_, Project>,
|
||||||
|
) {
|
||||||
|
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
||||||
|
.iter()
|
||||||
|
.map(Path::new)
|
||||||
|
.collect::<HashSet<_>>();
|
||||||
|
|
||||||
|
let prettier_config_file_changed = changes
|
||||||
|
.iter()
|
||||||
|
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
||||||
|
.filter(|(path, _, _)| {
|
||||||
|
!path
|
||||||
|
.components()
|
||||||
|
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
||||||
|
})
|
||||||
|
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
||||||
|
let current_worktree_id = worktree.read(cx).id();
|
||||||
|
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
||||||
|
log::info!(
|
||||||
|
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
||||||
|
);
|
||||||
|
let prettiers_to_reload =
|
||||||
|
self.prettiers_per_worktree
|
||||||
|
.get(¤t_worktree_id)
|
||||||
|
.iter()
|
||||||
|
.flat_map(|prettier_paths| prettier_paths.iter())
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|prettier_path| {
|
||||||
|
Some((
|
||||||
|
current_worktree_id,
|
||||||
|
Some(prettier_path.clone()),
|
||||||
|
self.prettier_instances.get(prettier_path)?.clone(),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
.chain(self.default_prettier.instance().map(|default_prettier| {
|
||||||
|
(current_worktree_id, None, default_prettier.clone())
|
||||||
|
}))
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
let _: Vec<()> = future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_instance)| {
|
||||||
|
async move {
|
||||||
|
if let Some(instance) = prettier_instance.prettier {
|
||||||
|
match instance.await {
|
||||||
|
Ok(prettier) => {
|
||||||
|
prettier.clear_cache().log_err().await;
|
||||||
|
},
|
||||||
|
Err(e) => {
|
||||||
|
match prettier_path {
|
||||||
|
Some(prettier_path) => log::error!(
|
||||||
|
"Failed to clear prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
|
||||||
|
),
|
||||||
|
None => log::error!(
|
||||||
|
"Failed to clear default prettier cache for worktree {worktree_id:?} on prettier settings update: {e:#}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.await;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prettier_instance_for_buffer(
|
||||||
|
&mut self,
|
||||||
|
buffer: &Model<Buffer>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) -> Task<Option<(Option<PathBuf>, PrettierTask)>> {
|
||||||
|
let buffer = buffer.read(cx);
|
||||||
|
let buffer_file = buffer.file();
|
||||||
|
let Some(buffer_language) = buffer.language() else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
if buffer_language.prettier_parser_name().is_none() {
|
||||||
|
return Task::ready(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.is_local() {
|
||||||
|
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
||||||
|
return Task::ready(None);
|
||||||
|
};
|
||||||
|
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
|
||||||
|
{
|
||||||
|
Some((worktree_id, buffer_path)) => {
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
||||||
|
return cx.spawn(|project, mut cx| async move {
|
||||||
|
match cx
|
||||||
|
.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
Prettier::locate_prettier_installation(
|
||||||
|
fs.as_ref(),
|
||||||
|
&installed_prettiers,
|
||||||
|
&buffer_path,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(ControlFlow::Break(())) => {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(None)) => {
|
||||||
|
let default_instance = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project
|
||||||
|
.prettiers_per_worktree
|
||||||
|
.entry(worktree_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(None);
|
||||||
|
project.default_prettier.prettier_task(
|
||||||
|
&node,
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
Some((None, default_instance?.log_err().await?))
|
||||||
|
}
|
||||||
|
Ok(ControlFlow::Continue(Some(prettier_dir))) => {
|
||||||
|
project
|
||||||
|
.update(&mut cx, |project, _| {
|
||||||
|
project
|
||||||
|
.prettiers_per_worktree
|
||||||
|
.entry(worktree_id)
|
||||||
|
.or_default()
|
||||||
|
.insert(Some(prettier_dir.clone()))
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
if let Some(prettier_task) = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
project.prettier_instances.get_mut(&prettier_dir).map(
|
||||||
|
|existing_instance| {
|
||||||
|
existing_instance.prettier_task(
|
||||||
|
&node,
|
||||||
|
Some(&prettier_dir),
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.ok()?
|
||||||
|
{
|
||||||
|
log::debug!(
|
||||||
|
"Found already started prettier in {prettier_dir:?}"
|
||||||
|
);
|
||||||
|
return Some((
|
||||||
|
Some(prettier_dir),
|
||||||
|
prettier_task?.await.log_err()?,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
||||||
|
let new_prettier_task = project
|
||||||
|
.update(&mut cx, |project, cx| {
|
||||||
|
let new_prettier_task = start_prettier(
|
||||||
|
node,
|
||||||
|
prettier_dir.clone(),
|
||||||
|
Some(worktree_id),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
project.prettier_instances.insert(
|
||||||
|
prettier_dir.clone(),
|
||||||
|
PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: Some(new_prettier_task.clone()),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
new_prettier_task
|
||||||
|
})
|
||||||
|
.ok()?;
|
||||||
|
Some((Some(prettier_dir), new_prettier_task))
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
log::error!("Failed to determine prettier path for buffer: {e:#}");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
None => {
|
||||||
|
let new_task = self.default_prettier.prettier_task(&node, None, cx);
|
||||||
|
return cx
|
||||||
|
.spawn(|_, _| async move { Some((None, new_task?.log_err().await?)) });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return Task::ready(None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
pub fn install_default_prettier(
|
||||||
|
&mut self,
|
||||||
|
_worktree: Option<WorktreeId>,
|
||||||
|
plugins: HashSet<&'static str>,
|
||||||
|
_cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
// suppress unused code warnings
|
||||||
|
let _ = install_prettier_packages;
|
||||||
|
let _ = save_prettier_server_file;
|
||||||
|
|
||||||
|
self.default_prettier.installed_plugins.extend(plugins);
|
||||||
|
self.default_prettier.prettier = PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: None,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
|
pub fn install_default_prettier(
|
||||||
|
&mut self,
|
||||||
|
worktree: Option<WorktreeId>,
|
||||||
|
mut new_plugins: HashSet<&'static str>,
|
||||||
|
cx: &mut ModelContext<Self>,
|
||||||
|
) {
|
||||||
|
let Some(node) = self.node.as_ref().cloned() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
log::info!("Initializing default prettier with plugins {new_plugins:?}");
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
|
||||||
|
self.worktree_for_id(worktree_id, cx)
|
||||||
|
.map(|worktree| worktree.read(cx).abs_path())
|
||||||
|
}) {
|
||||||
|
Some(locate_from) => {
|
||||||
|
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
||||||
|
cx.background_executor().spawn(async move {
|
||||||
|
Prettier::locate_prettier_installation(
|
||||||
|
fs.as_ref(),
|
||||||
|
&installed_prettiers,
|
||||||
|
locate_from.as_ref(),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
})
|
||||||
|
}
|
||||||
|
None => Task::ready(Ok(ControlFlow::Continue(None))),
|
||||||
|
};
|
||||||
|
new_plugins.retain(|plugin| !self.default_prettier.installed_plugins.contains(plugin));
|
||||||
|
let mut installation_attempt = 0;
|
||||||
|
let previous_installation_task = match &mut self.default_prettier.prettier {
|
||||||
|
PrettierInstallation::NotInstalled {
|
||||||
|
installation_task,
|
||||||
|
attempts,
|
||||||
|
not_installed_plugins,
|
||||||
|
} => {
|
||||||
|
installation_attempt = *attempts;
|
||||||
|
if installation_attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
*installation_task = None;
|
||||||
|
log::warn!(
|
||||||
|
"Default prettier installation had failed {installation_attempt} times, not attempting again",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
new_plugins.extend(not_installed_plugins.iter());
|
||||||
|
installation_task.clone()
|
||||||
|
}
|
||||||
|
PrettierInstallation::Installed { .. } => {
|
||||||
|
if new_plugins.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let plugins_to_install = new_plugins.clone();
|
||||||
|
let fs = Arc::clone(&self.fs);
|
||||||
|
let new_installation_task = cx
|
||||||
|
.spawn(|project, mut cx| async move {
|
||||||
|
match locate_prettier_installation
|
||||||
|
.await
|
||||||
|
.context("locate prettier installation")
|
||||||
|
.map_err(Arc::new)?
|
||||||
|
{
|
||||||
|
ControlFlow::Break(()) => return Ok(()),
|
||||||
|
ControlFlow::Continue(prettier_path) => {
|
||||||
|
if prettier_path.is_some() {
|
||||||
|
new_plugins.clear();
|
||||||
|
}
|
||||||
|
let mut needs_install = false;
|
||||||
|
if let Some(previous_installation_task) = previous_installation_task {
|
||||||
|
if let Err(e) = previous_installation_task.await {
|
||||||
|
log::error!("Failed to install default prettier: {e:#}");
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled { attempts, not_installed_plugins, .. } = &mut project.default_prettier.prettier {
|
||||||
|
*attempts += 1;
|
||||||
|
new_plugins.extend(not_installed_plugins.iter());
|
||||||
|
installation_attempt = *attempts;
|
||||||
|
needs_install = true;
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if installation_attempt > prettier::FAIL_THRESHOLD {
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
if let PrettierInstallation::NotInstalled { installation_task, .. } = &mut project.default_prettier.prettier {
|
||||||
|
*installation_task = None;
|
||||||
|
};
|
||||||
|
})?;
|
||||||
|
log::warn!(
|
||||||
|
"Default prettier installation had failed {installation_attempt} times, not attempting again",
|
||||||
|
);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
new_plugins.retain(|plugin| {
|
||||||
|
!project.default_prettier.installed_plugins.contains(plugin)
|
||||||
|
});
|
||||||
|
if let PrettierInstallation::NotInstalled { not_installed_plugins, .. } = &mut project.default_prettier.prettier {
|
||||||
|
not_installed_plugins.retain(|plugin| {
|
||||||
|
!project.default_prettier.installed_plugins.contains(plugin)
|
||||||
|
});
|
||||||
|
not_installed_plugins.extend(new_plugins.iter());
|
||||||
|
}
|
||||||
|
needs_install |= !new_plugins.is_empty();
|
||||||
|
})?;
|
||||||
|
if needs_install {
|
||||||
|
let installed_plugins = new_plugins.clone();
|
||||||
|
cx.background_executor()
|
||||||
|
.spawn(async move {
|
||||||
|
save_prettier_server_file(fs.as_ref()).await?;
|
||||||
|
install_prettier_packages(new_plugins, node).await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.context("prettier & plugins install")
|
||||||
|
.map_err(Arc::new)?;
|
||||||
|
log::info!("Initialized prettier with plugins: {installed_plugins:?}");
|
||||||
|
project.update(&mut cx, |project, _| {
|
||||||
|
project.default_prettier.prettier =
|
||||||
|
PrettierInstallation::Installed(PrettierInstance {
|
||||||
|
attempt: 0,
|
||||||
|
prettier: None,
|
||||||
|
});
|
||||||
|
project.default_prettier
|
||||||
|
.installed_plugins
|
||||||
|
.extend(installed_plugins);
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.shared();
|
||||||
|
self.default_prettier.prettier = PrettierInstallation::NotInstalled {
|
||||||
|
attempts: installation_attempt,
|
||||||
|
installation_task: Some(new_installation_task),
|
||||||
|
not_installed_plugins: plugins_to_install,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
mod ignore;
|
mod ignore;
|
||||||
mod lsp_command;
|
mod lsp_command;
|
||||||
|
mod prettier_support;
|
||||||
pub mod project_settings;
|
pub mod project_settings;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod terminals;
|
pub mod terminals;
|
||||||
|
@ -20,7 +21,7 @@ use futures::{
|
||||||
mpsc::{self, UnboundedReceiver},
|
mpsc::{self, UnboundedReceiver},
|
||||||
oneshot,
|
oneshot,
|
||||||
},
|
},
|
||||||
future::{self, try_join_all, Shared},
|
future::{try_join_all, Shared},
|
||||||
stream::FuturesUnordered,
|
stream::FuturesUnordered,
|
||||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||||
};
|
};
|
||||||
|
@ -31,9 +32,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use language::{
|
use language::{
|
||||||
language_settings::{
|
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
|
||||||
},
|
|
||||||
point_to_lsp,
|
point_to_lsp,
|
||||||
proto::{
|
proto::{
|
||||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||||
|
@ -54,7 +53,7 @@ use lsp_command::*;
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use prettier::Prettier;
|
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||||
use project_settings::{LspSettings, ProjectSettings};
|
use project_settings::{LspSettings, ProjectSettings};
|
||||||
use rand::prelude::*;
|
use rand::prelude::*;
|
||||||
use search::SearchQuery;
|
use search::SearchQuery;
|
||||||
|
@ -70,7 +69,7 @@ use std::{
|
||||||
hash::Hash,
|
hash::Hash,
|
||||||
mem,
|
mem,
|
||||||
num::NonZeroU32,
|
num::NonZeroU32,
|
||||||
ops::{ControlFlow, Range},
|
ops::Range,
|
||||||
path::{self, Component, Path, PathBuf},
|
path::{self, Component, Path, PathBuf},
|
||||||
process::Stdio,
|
process::Stdio,
|
||||||
str,
|
str,
|
||||||
|
@ -83,11 +82,8 @@ use std::{
|
||||||
use terminals::Terminals;
|
use terminals::Terminals;
|
||||||
use text::Anchor;
|
use text::Anchor;
|
||||||
use util::{
|
use util::{
|
||||||
debug_panic, defer,
|
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||||
http::HttpClient,
|
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||||
merge_json_value_into,
|
|
||||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
|
||||||
post_inc, ResultExt, TryFutureExt as _,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
pub use fs::*;
|
pub use fs::*;
|
||||||
|
@ -166,16 +162,9 @@ pub struct Project {
|
||||||
copilot_log_subscription: Option<lsp::Subscription>,
|
copilot_log_subscription: Option<lsp::Subscription>,
|
||||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||||
node: Option<Arc<dyn NodeRuntime>>,
|
node: Option<Arc<dyn NodeRuntime>>,
|
||||||
default_prettier: Option<DefaultPrettier>,
|
default_prettier: DefaultPrettier,
|
||||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||||
prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||||
}
|
|
||||||
|
|
||||||
struct DefaultPrettier {
|
|
||||||
instance: Option<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
|
||||||
installation_process: Option<Shared<Task<Result<(), Arc<anyhow::Error>>>>>,
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
installed_plugins: HashSet<&'static str>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct DelayedDebounced {
|
struct DelayedDebounced {
|
||||||
|
@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate {
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Currently, formatting operations are represented differently depending on
|
||||||
|
// whether they come from a language server or an external command.
|
||||||
|
enum FormatOperation {
|
||||||
|
Lsp(Vec<(Range<Anchor>, String)>),
|
||||||
|
External(Diff),
|
||||||
|
Prettier(Diff),
|
||||||
|
}
|
||||||
|
|
||||||
impl FormatTrigger {
|
impl FormatTrigger {
|
||||||
fn from_proto(value: i32) -> FormatTrigger {
|
fn from_proto(value: i32) -> FormatTrigger {
|
||||||
match value {
|
match value {
|
||||||
|
@ -689,7 +686,7 @@ impl Project {
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
||||||
node: Some(node),
|
node: Some(node),
|
||||||
default_prettier: None,
|
default_prettier: DefaultPrettier::default(),
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
}
|
}
|
||||||
|
@ -792,7 +789,7 @@ impl Project {
|
||||||
copilot_log_subscription: None,
|
copilot_log_subscription: None,
|
||||||
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
||||||
node: None,
|
node: None,
|
||||||
default_prettier: None,
|
default_prettier: DefaultPrettier::default(),
|
||||||
prettiers_per_worktree: HashMap::default(),
|
prettiers_per_worktree: HashMap::default(),
|
||||||
prettier_instances: HashMap::default(),
|
prettier_instances: HashMap::default(),
|
||||||
};
|
};
|
||||||
|
@ -965,8 +962,19 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut prettier_plugins_by_worktree = HashMap::default();
|
||||||
for (worktree, language, settings) in language_formatters_to_check {
|
for (worktree, language, settings) in language_formatters_to_check {
|
||||||
self.install_default_formatters(worktree, &language, &settings, cx);
|
if let Some(plugins) =
|
||||||
|
prettier_support::prettier_plugins_for_language(&language, &settings)
|
||||||
|
{
|
||||||
|
prettier_plugins_by_worktree
|
||||||
|
.entry(worktree)
|
||||||
|
.or_insert_with(|| HashSet::default())
|
||||||
|
.extend(plugins);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (worktree, prettier_plugins) in prettier_plugins_by_worktree {
|
||||||
|
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start all the newly-enabled language servers.
|
// Start all the newly-enabled language servers.
|
||||||
|
@ -2722,8 +2730,11 @@ impl Project {
|
||||||
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||||
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||||
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||||
|
if let Some(prettier_plugins) =
|
||||||
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
prettier_support::prettier_plugins_for_language(&new_language, &settings)
|
||||||
|
{
|
||||||
|
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||||
|
};
|
||||||
if let Some(file) = buffer_file {
|
if let Some(file) = buffer_file {
|
||||||
let worktree = file.worktree.clone();
|
let worktree = file.worktree.clone();
|
||||||
if let Some(tree) = worktree.read(cx).as_local() {
|
if let Some(tree) = worktree.read(cx).as_local() {
|
||||||
|
@ -4126,7 +4137,8 @@ impl Project {
|
||||||
this.buffers_being_formatted
|
this.buffers_being_formatted
|
||||||
.remove(&buffer.read(cx).remote_id());
|
.remove(&buffer.read(cx).remote_id());
|
||||||
}
|
}
|
||||||
}).ok();
|
})
|
||||||
|
.ok();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -4138,8 +4150,6 @@ impl Project {
|
||||||
|
|
||||||
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
let remove_trailing_whitespace = settings.remove_trailing_whitespace_on_save;
|
||||||
let ensure_final_newline = settings.ensure_final_newline_on_save;
|
let ensure_final_newline = settings.ensure_final_newline_on_save;
|
||||||
let format_on_save = settings.format_on_save.clone();
|
|
||||||
let formatter = settings.formatter.clone();
|
|
||||||
let tab_size = settings.tab_size;
|
let tab_size = settings.tab_size;
|
||||||
|
|
||||||
// First, format buffer's whitespace according to the settings.
|
// First, format buffer's whitespace according to the settings.
|
||||||
|
@ -4164,18 +4174,10 @@ impl Project {
|
||||||
buffer.end_transaction(cx)
|
buffer.end_transaction(cx)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
// Currently, formatting operations are represented differently depending on
|
|
||||||
// whether they come from a language server or an external command.
|
|
||||||
enum FormatOperation {
|
|
||||||
Lsp(Vec<(Range<Anchor>, String)>),
|
|
||||||
External(Diff),
|
|
||||||
Prettier(Diff),
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply language-specific formatting using either a language server
|
// Apply language-specific formatting using either a language server
|
||||||
// or external command.
|
// or external command.
|
||||||
let mut format_operation = None;
|
let mut format_operation = None;
|
||||||
match (formatter, format_on_save) {
|
match (&settings.formatter, &settings.format_on_save) {
|
||||||
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
||||||
|
|
||||||
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
||||||
|
@ -4220,46 +4222,11 @@ impl Project {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
if let Some((prettier_path, prettier_task)) = project
|
if let Some(new_operation) =
|
||||||
.update(&mut cx, |project, cx| {
|
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||||
project.prettier_instance_for_buffer(buffer, cx)
|
|
||||||
})?.await {
|
|
||||||
match prettier_task.await
|
|
||||||
{
|
|
||||||
Ok(prettier) => {
|
|
||||||
let buffer_path = buffer.update(&mut cx, |buffer, cx| {
|
|
||||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
|
||||||
})?;
|
|
||||||
format_operation = Some(FormatOperation::Prettier(
|
|
||||||
prettier
|
|
||||||
.format(buffer, buffer_path, &mut cx)
|
|
||||||
.await
|
.await
|
||||||
.context("formatting via prettier")?,
|
{
|
||||||
));
|
format_operation = Some(new_operation);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
project.prettier_instances.remove(prettier_path);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if let Some(default_prettier) = project.default_prettier.as_mut() {
|
|
||||||
default_prettier.instance = None;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if let Some((language_server, buffer_abs_path)) =
|
} else if let Some((language_server, buffer_abs_path)) =
|
||||||
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||||
{
|
{
|
||||||
|
@ -4277,47 +4244,12 @@ impl Project {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
(Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
|
||||||
if let Some((prettier_path, prettier_task)) = project
|
if let Some(new_operation) =
|
||||||
.update(&mut cx, |project, cx| {
|
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||||
project.prettier_instance_for_buffer(buffer, cx)
|
|
||||||
})?.await {
|
|
||||||
match prettier_task.await
|
|
||||||
{
|
|
||||||
Ok(prettier) => {
|
|
||||||
let buffer_path = buffer.update(&mut cx, |buffer, cx| {
|
|
||||||
File::from_dyn(buffer.file()).map(|file| file.abs_path(cx))
|
|
||||||
})?;
|
|
||||||
format_operation = Some(FormatOperation::Prettier(
|
|
||||||
prettier
|
|
||||||
.format(buffer, buffer_path, &mut cx)
|
|
||||||
.await
|
.await
|
||||||
.context("formatting via prettier")?,
|
{
|
||||||
));
|
format_operation = Some(new_operation);
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
project.update(&mut cx, |project, _| {
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
project.prettier_instances.remove(prettier_path);
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
if let Some(default_prettier) = project.default_prettier.as_mut() {
|
|
||||||
default_prettier.instance = None;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
match &prettier_path {
|
|
||||||
Some(prettier_path) => {
|
|
||||||
log::error!("Failed to create prettier instance from {prettier_path:?} for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to create default prettier instance for buffer during autoformatting: {e:#}");
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -6638,84 +6570,6 @@ impl Project {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn update_prettier_settings(
|
|
||||||
&self,
|
|
||||||
worktree: &Model<Worktree>,
|
|
||||||
changes: &[(Arc<Path>, ProjectEntryId, PathChange)],
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) {
|
|
||||||
let prettier_config_files = Prettier::CONFIG_FILE_NAMES
|
|
||||||
.iter()
|
|
||||||
.map(Path::new)
|
|
||||||
.collect::<HashSet<_>>();
|
|
||||||
|
|
||||||
let prettier_config_file_changed = changes
|
|
||||||
.iter()
|
|
||||||
.filter(|(_, _, change)| !matches!(change, PathChange::Loaded))
|
|
||||||
.filter(|(path, _, _)| {
|
|
||||||
!path
|
|
||||||
.components()
|
|
||||||
.any(|component| component.as_os_str().to_string_lossy() == "node_modules")
|
|
||||||
})
|
|
||||||
.find(|(path, _, _)| prettier_config_files.contains(path.as_ref()));
|
|
||||||
let current_worktree_id = worktree.read(cx).id();
|
|
||||||
if let Some((config_path, _, _)) = prettier_config_file_changed {
|
|
||||||
log::info!(
|
|
||||||
"Prettier config file {config_path:?} changed, reloading prettier instances for worktree {current_worktree_id}"
|
|
||||||
);
|
|
||||||
let prettiers_to_reload = self
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.get(¤t_worktree_id)
|
|
||||||
.iter()
|
|
||||||
.flat_map(|prettier_paths| prettier_paths.iter())
|
|
||||||
.flatten()
|
|
||||||
.filter_map(|prettier_path| {
|
|
||||||
Some((
|
|
||||||
current_worktree_id,
|
|
||||||
Some(prettier_path.clone()),
|
|
||||||
self.prettier_instances.get(prettier_path)?.clone(),
|
|
||||||
))
|
|
||||||
})
|
|
||||||
.chain(self.default_prettier.iter().filter_map(|default_prettier| {
|
|
||||||
Some((
|
|
||||||
current_worktree_id,
|
|
||||||
None,
|
|
||||||
default_prettier.instance.clone()?,
|
|
||||||
))
|
|
||||||
}))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
for task_result in future::join_all(prettiers_to_reload.into_iter().map(|(worktree_id, prettier_path, prettier_task)| {
|
|
||||||
async move {
|
|
||||||
prettier_task.await?
|
|
||||||
.clear_cache()
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
match prettier_path {
|
|
||||||
Some(prettier_path) => format!(
|
|
||||||
"clearing prettier {prettier_path:?} cache for worktree {worktree_id:?} on prettier settings update"
|
|
||||||
),
|
|
||||||
None => format!(
|
|
||||||
"clearing default prettier cache for worktree {worktree_id:?} on prettier settings update"
|
|
||||||
),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.map_err(Arc::new)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
if let Err(e) = task_result {
|
|
||||||
log::error!("Failed to clear cache for prettier: {e:#}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
pub fn set_active_path(&mut self, entry: Option<ProjectPath>, cx: &mut ModelContext<Self>) {
|
||||||
let new_active_entry = entry.and_then(|project_path| {
|
let new_active_entry = entry.and_then(|project_path| {
|
||||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||||
|
@ -8579,486 +8433,6 @@ impl Project {
|
||||||
Vec::new()
|
Vec::new()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prettier_instance_for_buffer(
|
|
||||||
&mut self,
|
|
||||||
buffer: &Model<Buffer>,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) -> Task<
|
|
||||||
Option<(
|
|
||||||
Option<PathBuf>,
|
|
||||||
Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>,
|
|
||||||
)>,
|
|
||||||
> {
|
|
||||||
let buffer = buffer.read(cx);
|
|
||||||
let buffer_file = buffer.file();
|
|
||||||
let Some(buffer_language) = buffer.language() else {
|
|
||||||
return Task::ready(None);
|
|
||||||
};
|
|
||||||
if buffer_language.prettier_parser_name().is_none() {
|
|
||||||
return Task::ready(None);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.is_local() {
|
|
||||||
let Some(node) = self.node.as_ref().map(Arc::clone) else {
|
|
||||||
return Task::ready(None);
|
|
||||||
};
|
|
||||||
match File::from_dyn(buffer_file).map(|file| (file.worktree_id(cx), file.abs_path(cx)))
|
|
||||||
{
|
|
||||||
Some((worktree_id, buffer_path)) => {
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
|
||||||
return cx.spawn(|project, mut cx| async move {
|
|
||||||
match cx
|
|
||||||
.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
Prettier::locate_prettier_installation(
|
|
||||||
fs.as_ref(),
|
|
||||||
&installed_prettiers,
|
|
||||||
&buffer_path,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
Ok(ControlFlow::Break(())) => {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
Ok(ControlFlow::Continue(None)) => {
|
|
||||||
match project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.entry(worktree_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(None);
|
|
||||||
project.default_prettier.as_ref().and_then(
|
|
||||||
|default_prettier| default_prettier.instance.clone(),
|
|
||||||
)
|
|
||||||
}) {
|
|
||||||
Ok(Some(old_task)) => Some((None, old_task)),
|
|
||||||
Ok(None) => {
|
|
||||||
match project.update(&mut cx, |_, cx| {
|
|
||||||
start_default_prettier(node, Some(worktree_id), cx)
|
|
||||||
}) {
|
|
||||||
Ok(new_default_prettier) => {
|
|
||||||
return Some((None, new_default_prettier.await))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
Some((
|
|
||||||
None,
|
|
||||||
Task::ready(Err(Arc::new(e.context("project is gone during default prettier startup"))))
|
|
||||||
.shared(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Err(e) => Some((None, Task::ready(Err(Arc::new(e.context("project is gone during default prettier checks"))))
|
|
||||||
.shared())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(ControlFlow::Continue(Some(prettier_dir))) => {
|
|
||||||
match project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.prettiers_per_worktree
|
|
||||||
.entry(worktree_id)
|
|
||||||
.or_default()
|
|
||||||
.insert(Some(prettier_dir.clone()));
|
|
||||||
project.prettier_instances.get(&prettier_dir).cloned()
|
|
||||||
}) {
|
|
||||||
Ok(Some(existing_prettier)) => {
|
|
||||||
log::debug!(
|
|
||||||
"Found already started prettier in {prettier_dir:?}"
|
|
||||||
);
|
|
||||||
return Some((Some(prettier_dir), existing_prettier));
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Some((
|
|
||||||
Some(prettier_dir),
|
|
||||||
Task::ready(Err(Arc::new(e.context("project is gone during custom prettier checks"))))
|
|
||||||
.shared(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => {},
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Found prettier in {prettier_dir:?}, starting.");
|
|
||||||
let new_prettier_task =
|
|
||||||
match project.update(&mut cx, |project, cx| {
|
|
||||||
let new_prettier_task = start_prettier(
|
|
||||||
node,
|
|
||||||
prettier_dir.clone(),
|
|
||||||
Some(worktree_id),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
project.prettier_instances.insert(
|
|
||||||
prettier_dir.clone(),
|
|
||||||
new_prettier_task.clone(),
|
|
||||||
);
|
|
||||||
new_prettier_task
|
|
||||||
}) {
|
|
||||||
Ok(task) => task,
|
|
||||||
Err(e) => return Some((
|
|
||||||
Some(prettier_dir),
|
|
||||||
Task::ready(Err(Arc::new(e.context("project is gone during custom prettier startup"))))
|
|
||||||
.shared()
|
|
||||||
)),
|
|
||||||
};
|
|
||||||
Some((Some(prettier_dir), new_prettier_task))
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
return Some((
|
|
||||||
None,
|
|
||||||
Task::ready(Err(Arc::new(
|
|
||||||
e.context("determining prettier path"),
|
|
||||||
)))
|
|
||||||
.shared(),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
None => {
|
|
||||||
let started_default_prettier = self
|
|
||||||
.default_prettier
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|default_prettier| default_prettier.instance.clone());
|
|
||||||
match started_default_prettier {
|
|
||||||
Some(old_task) => return Task::ready(Some((None, old_task))),
|
|
||||||
None => {
|
|
||||||
let new_task = start_default_prettier(node, None, cx);
|
|
||||||
return cx.spawn(|_, _| async move { Some((None, new_task.await)) });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if self.remote_id().is_some() {
|
|
||||||
return Task::ready(None);
|
|
||||||
} else {
|
|
||||||
Task::ready(Some((
|
|
||||||
None,
|
|
||||||
Task::ready(Err(Arc::new(anyhow!("project does not have a remote id")))).shared(),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
|
||||||
fn install_default_formatters(
|
|
||||||
&mut self,
|
|
||||||
_: Option<WorktreeId>,
|
|
||||||
_: &Language,
|
|
||||||
_: &LanguageSettings,
|
|
||||||
_: &mut ModelContext<Self>,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
fn install_default_formatters(
|
|
||||||
&mut self,
|
|
||||||
worktree: Option<WorktreeId>,
|
|
||||||
new_language: &Language,
|
|
||||||
language_settings: &LanguageSettings,
|
|
||||||
cx: &mut ModelContext<Self>,
|
|
||||||
) {
|
|
||||||
match &language_settings.formatter {
|
|
||||||
Formatter::Prettier { .. } | Formatter::Auto => {}
|
|
||||||
Formatter::LanguageServer | Formatter::External { .. } => return,
|
|
||||||
};
|
|
||||||
let Some(node) = self.node.as_ref().cloned() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut prettier_plugins = None;
|
|
||||||
if new_language.prettier_parser_name().is_some() {
|
|
||||||
prettier_plugins
|
|
||||||
.get_or_insert_with(|| HashSet::<&'static str>::default())
|
|
||||||
.extend(
|
|
||||||
new_language
|
|
||||||
.lsp_adapters()
|
|
||||||
.iter()
|
|
||||||
.flat_map(|adapter| adapter.prettier_plugins()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
let Some(prettier_plugins) = prettier_plugins else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let locate_prettier_installation = match worktree.and_then(|worktree_id| {
|
|
||||||
self.worktree_for_id(worktree_id, cx)
|
|
||||||
.map(|worktree| worktree.read(cx).abs_path())
|
|
||||||
}) {
|
|
||||||
Some(locate_from) => {
|
|
||||||
let installed_prettiers = self.prettier_instances.keys().cloned().collect();
|
|
||||||
cx.background_executor().spawn(async move {
|
|
||||||
Prettier::locate_prettier_installation(
|
|
||||||
fs.as_ref(),
|
|
||||||
&installed_prettiers,
|
|
||||||
locate_from.as_ref(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
})
|
|
||||||
}
|
|
||||||
None => Task::ready(Ok(ControlFlow::Break(()))),
|
|
||||||
};
|
|
||||||
let mut plugins_to_install = prettier_plugins;
|
|
||||||
let previous_installation_process =
|
|
||||||
if let Some(default_prettier) = &mut self.default_prettier {
|
|
||||||
plugins_to_install
|
|
||||||
.retain(|plugin| !default_prettier.installed_plugins.contains(plugin));
|
|
||||||
if plugins_to_install.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
default_prettier.installation_process.clone()
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let fs = Arc::clone(&self.fs);
|
|
||||||
let default_prettier = self
|
|
||||||
.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: None,
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
});
|
|
||||||
default_prettier.installation_process = Some(
|
|
||||||
cx.spawn(|this, mut cx| async move {
|
|
||||||
match locate_prettier_installation
|
|
||||||
.await
|
|
||||||
.context("locate prettier installation")
|
|
||||||
.map_err(Arc::new)?
|
|
||||||
{
|
|
||||||
ControlFlow::Break(()) => return Ok(()),
|
|
||||||
ControlFlow::Continue(Some(_non_default_prettier)) => return Ok(()),
|
|
||||||
ControlFlow::Continue(None) => {
|
|
||||||
let mut needs_install = match previous_installation_process {
|
|
||||||
Some(previous_installation_process) => {
|
|
||||||
previous_installation_process.await.is_err()
|
|
||||||
}
|
|
||||||
None => true,
|
|
||||||
};
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
if let Some(default_prettier) = &mut this.default_prettier {
|
|
||||||
plugins_to_install.retain(|plugin| {
|
|
||||||
!default_prettier.installed_plugins.contains(plugin)
|
|
||||||
});
|
|
||||||
needs_install |= !plugins_to_install.is_empty();
|
|
||||||
}
|
|
||||||
})?;
|
|
||||||
if needs_install {
|
|
||||||
let installed_plugins = plugins_to_install.clone();
|
|
||||||
cx.background_executor()
|
|
||||||
.spawn(async move {
|
|
||||||
install_default_prettier(plugins_to_install, node, fs).await
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.context("prettier & plugins install")
|
|
||||||
.map_err(Arc::new)?;
|
|
||||||
this.update(&mut cx, |this, _| {
|
|
||||||
let default_prettier =
|
|
||||||
this.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: Some(
|
|
||||||
Task::ready(Ok(())).shared(),
|
|
||||||
),
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
});
|
|
||||||
default_prettier.instance = None;
|
|
||||||
default_prettier.installed_plugins.extend(installed_plugins);
|
|
||||||
})?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
.shared(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_default_prettier(
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) -> Task<Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>> {
|
|
||||||
cx.spawn(|project, mut cx| async move {
|
|
||||||
loop {
|
|
||||||
let default_prettier_installing = match project.update(&mut cx, |project, _| {
|
|
||||||
project
|
|
||||||
.default_prettier
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|default_prettier| default_prettier.installation_process.clone())
|
|
||||||
}) {
|
|
||||||
Ok(installation) => installation,
|
|
||||||
Err(e) => {
|
|
||||||
return Task::ready(Err(Arc::new(
|
|
||||||
e.context("project is gone during default prettier installation"),
|
|
||||||
)))
|
|
||||||
.shared()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
match default_prettier_installing {
|
|
||||||
Some(installation_task) => {
|
|
||||||
if installation_task.await.is_ok() {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => break,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
match project.update(&mut cx, |project, cx| {
|
|
||||||
match project
|
|
||||||
.default_prettier
|
|
||||||
.as_mut()
|
|
||||||
.and_then(|default_prettier| default_prettier.instance.as_mut())
|
|
||||||
{
|
|
||||||
Some(default_prettier) => default_prettier.clone(),
|
|
||||||
None => {
|
|
||||||
let new_default_prettier =
|
|
||||||
start_prettier(node, DEFAULT_PRETTIER_DIR.clone(), worktree_id, cx);
|
|
||||||
project
|
|
||||||
.default_prettier
|
|
||||||
.get_or_insert_with(|| DefaultPrettier {
|
|
||||||
instance: None,
|
|
||||||
installation_process: None,
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
installed_plugins: HashSet::default(),
|
|
||||||
})
|
|
||||||
.instance = Some(new_default_prettier.clone());
|
|
||||||
new_default_prettier
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}) {
|
|
||||||
Ok(task) => task,
|
|
||||||
Err(e) => Task::ready(Err(Arc::new(
|
|
||||||
e.context("project is gone during default prettier startup"),
|
|
||||||
)))
|
|
||||||
.shared(),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn start_prettier(
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
prettier_dir: PathBuf,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
cx: &mut ModelContext<'_, Project>,
|
|
||||||
) -> Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>> {
|
|
||||||
cx.spawn(|project, mut cx| async move {
|
|
||||||
let new_server_id = project.update(&mut cx, |project, _| {
|
|
||||||
project.languages.next_language_server_id()
|
|
||||||
})?;
|
|
||||||
let new_prettier = Prettier::start(new_server_id, prettier_dir, node, cx.clone())
|
|
||||||
.await
|
|
||||||
.context("default prettier spawn")
|
|
||||||
.map(Arc::new)
|
|
||||||
.map_err(Arc::new)?;
|
|
||||||
register_new_prettier(&project, &new_prettier, worktree_id, new_server_id, &mut cx);
|
|
||||||
Ok(new_prettier)
|
|
||||||
})
|
|
||||||
.shared()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_new_prettier(
|
|
||||||
project: &WeakModel<Project>,
|
|
||||||
prettier: &Prettier,
|
|
||||||
worktree_id: Option<WorktreeId>,
|
|
||||||
new_server_id: LanguageServerId,
|
|
||||||
cx: &mut AsyncAppContext,
|
|
||||||
) {
|
|
||||||
let prettier_dir = prettier.prettier_dir();
|
|
||||||
let is_default = prettier.is_default();
|
|
||||||
if is_default {
|
|
||||||
log::info!("Started default prettier in {prettier_dir:?}");
|
|
||||||
} else {
|
|
||||||
log::info!("Started prettier in {prettier_dir:?}");
|
|
||||||
}
|
|
||||||
if let Some(prettier_server) = prettier.server() {
|
|
||||||
project
|
|
||||||
.update(cx, |project, cx| {
|
|
||||||
let name = if is_default {
|
|
||||||
LanguageServerName(Arc::from("prettier (default)"))
|
|
||||||
} else {
|
|
||||||
let worktree_path = worktree_id
|
|
||||||
.and_then(|id| project.worktree_for_id(id, cx))
|
|
||||||
.map(|worktree| worktree.update(cx, |worktree, _| worktree.abs_path()));
|
|
||||||
let name = match worktree_path {
|
|
||||||
Some(worktree_path) => {
|
|
||||||
if prettier_dir == worktree_path.as_ref() {
|
|
||||||
let name = prettier_dir
|
|
||||||
.file_name()
|
|
||||||
.and_then(|name| name.to_str())
|
|
||||||
.unwrap_or_default();
|
|
||||||
format!("prettier ({name})")
|
|
||||||
} else {
|
|
||||||
let dir_to_display = prettier_dir
|
|
||||||
.strip_prefix(worktree_path.as_ref())
|
|
||||||
.ok()
|
|
||||||
.unwrap_or(prettier_dir);
|
|
||||||
format!("prettier ({})", dir_to_display.display())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None => format!("prettier ({})", prettier_dir.display()),
|
|
||||||
};
|
|
||||||
LanguageServerName(Arc::from(name))
|
|
||||||
};
|
|
||||||
project
|
|
||||||
.supplementary_language_servers
|
|
||||||
.insert(new_server_id, (name, Arc::clone(prettier_server)));
|
|
||||||
cx.emit(Event::LanguageServerAdded(new_server_id));
|
|
||||||
})
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
|
||||||
async fn install_default_prettier(
|
|
||||||
plugins_to_install: HashSet<&'static str>,
|
|
||||||
node: Arc<dyn NodeRuntime>,
|
|
||||||
fs: Arc<dyn Fs>,
|
|
||||||
) -> anyhow::Result<()> {
|
|
||||||
let prettier_wrapper_path = DEFAULT_PRETTIER_DIR.join(prettier::PRETTIER_SERVER_FILE);
|
|
||||||
// method creates parent directory if it doesn't exist
|
|
||||||
fs.save(
|
|
||||||
&prettier_wrapper_path,
|
|
||||||
&text::Rope::from(prettier::PRETTIER_SERVER_JS),
|
|
||||||
text::LineEnding::Unix,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!(
|
|
||||||
"writing {} file at {prettier_wrapper_path:?}",
|
|
||||||
prettier::PRETTIER_SERVER_FILE
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let packages_to_versions =
|
|
||||||
future::try_join_all(plugins_to_install.iter().chain(Some(&"prettier")).map(
|
|
||||||
|package_name| async {
|
|
||||||
let returned_package_name = package_name.to_string();
|
|
||||||
let latest_version = node
|
|
||||||
.npm_package_latest_version(package_name)
|
|
||||||
.await
|
|
||||||
.with_context(|| {
|
|
||||||
format!("fetching latest npm version for package {returned_package_name}")
|
|
||||||
})?;
|
|
||||||
anyhow::Ok((returned_package_name, latest_version))
|
|
||||||
},
|
|
||||||
))
|
|
||||||
.await
|
|
||||||
.context("fetching latest npm versions")?;
|
|
||||||
|
|
||||||
log::info!("Fetching default prettier and plugins: {packages_to_versions:?}");
|
|
||||||
let borrowed_packages = packages_to_versions
|
|
||||||
.iter()
|
|
||||||
.map(|(package, version)| (package.as_str(), version.as_str()))
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
node.npm_install_packages(DEFAULT_PRETTIER_DIR.as_path(), &borrowed_packages)
|
|
||||||
.await
|
|
||||||
.context("fetching formatter packages")?;
|
|
||||||
anyhow::Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn subscribe_for_copilot_events(
|
fn subscribe_for_copilot_events(
|
||||||
|
|
|
@ -41,8 +41,7 @@ impl FileAssociations {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
|
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
|
||||||
maybe!({
|
|
||||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||||
|
|
||||||
// FIXME: Associate a type with the languages and have the file's langauge
|
// FIXME: Associate a type with the languages and have the file's langauge
|
||||||
|
@ -56,12 +55,9 @@ impl FileAssociations {
|
||||||
.map(|type_config| type_config.icon.clone())
|
.map(|type_config| type_config.icon.clone())
|
||||||
})
|
})
|
||||||
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
|
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
|
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||||
maybe!({
|
|
||||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||||
|
|
||||||
let key = if expanded {
|
let key = if expanded {
|
||||||
|
@ -73,12 +69,9 @@ impl FileAssociations {
|
||||||
this.types
|
this.types
|
||||||
.get(key)
|
.get(key)
|
||||||
.map(|type_config| type_config.icon.clone())
|
.map(|type_config| type_config.icon.clone())
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
|
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||||
maybe!({
|
|
||||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||||
|
|
||||||
let key = if expanded {
|
let key = if expanded {
|
||||||
|
@ -90,7 +83,5 @@ impl FileAssociations {
|
||||||
this.types
|
this.types
|
||||||
.get(key)
|
.get(key)
|
||||||
.map(|type_config| type_config.icon.clone())
|
.map(|type_config| type_config.icon.clone())
|
||||||
})
|
|
||||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1283,16 +1283,16 @@ impl ProjectPanel {
|
||||||
let icon = match entry.kind {
|
let icon = match entry.kind {
|
||||||
EntryKind::File(_) => {
|
EntryKind::File(_) => {
|
||||||
if show_file_icons {
|
if show_file_icons {
|
||||||
Some(FileAssociations::get_icon(&entry.path, cx))
|
FileAssociations::get_icon(&entry.path, cx)
|
||||||
} else {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
if show_folder_icons {
|
if show_folder_icons {
|
||||||
Some(FileAssociations::get_folder_icon(is_expanded, cx))
|
FileAssociations::get_folder_icon(is_expanded, cx)
|
||||||
} else {
|
} else {
|
||||||
Some(FileAssociations::get_chevron_icon(is_expanded, cx))
|
FileAssociations::get_chevron_icon(is_expanded, cx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use gpui::{IntoElement, MouseDownEvent, WindowContext};
|
use gpui::{ClickEvent, IntoElement, WindowContext};
|
||||||
use ui::{Button, ButtonVariant, IconButton};
|
use ui::{Button, ButtonVariant, IconButton};
|
||||||
|
|
||||||
use crate::mode::SearchMode;
|
use crate::mode::SearchMode;
|
||||||
|
@ -6,7 +6,7 @@ use crate::mode::SearchMode;
|
||||||
pub(super) fn render_nav_button(
|
pub(super) fn render_nav_button(
|
||||||
icon: ui::Icon,
|
icon: ui::Icon,
|
||||||
_active: bool,
|
_active: bool,
|
||||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
) -> impl IntoElement {
|
) -> impl IntoElement {
|
||||||
// let tooltip_style = cx.theme().tooltip.clone();
|
// let tooltip_style = cx.theme().tooltip.clone();
|
||||||
// let cursor_style = if active {
|
// let cursor_style = if active {
|
||||||
|
@ -21,7 +21,7 @@ pub(super) fn render_nav_button(
|
||||||
pub(crate) fn render_search_mode_button(
|
pub(crate) fn render_search_mode_button(
|
||||||
mode: SearchMode,
|
mode: SearchMode,
|
||||||
is_active: bool,
|
is_active: bool,
|
||||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
) -> Button {
|
) -> Button {
|
||||||
let button_variant = if is_active {
|
let button_variant = if is_active {
|
||||||
ButtonVariant::Filled
|
ButtonVariant::Filled
|
||||||
|
|
|
@ -2,7 +2,7 @@ use gpui::{
|
||||||
actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
|
actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
|
|
||||||
actions!(ActionA, ActionB, ActionC);
|
actions!(ActionA, ActionB, ActionC);
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use picker::{Picker, PickerDelegate};
|
use picker::{Picker, PickerDelegate};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
use ui::{Label, ListItem};
|
use ui::{Label, ListItem};
|
||||||
|
|
||||||
pub struct PickerStory {
|
pub struct PickerStory {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
use ui::Tooltip;
|
use ui::Tooltip;
|
||||||
|
|
||||||
pub struct ScrollStory;
|
pub struct ScrollStory;
|
||||||
|
|
|
@ -19,7 +19,6 @@ pub enum ComponentStory {
|
||||||
Focus,
|
Focus,
|
||||||
Icon,
|
Icon,
|
||||||
IconButton,
|
IconButton,
|
||||||
Input,
|
|
||||||
Keybinding,
|
Keybinding,
|
||||||
Label,
|
Label,
|
||||||
ListItem,
|
ListItem,
|
||||||
|
@ -39,7 +38,6 @@ impl ComponentStory {
|
||||||
Self::Focus => FocusStory::view(cx).into(),
|
Self::Focus => FocusStory::view(cx).into(),
|
||||||
Self::Icon => cx.build_view(|_| ui::IconStory).into(),
|
Self::Icon => cx.build_view(|_| ui::IconStory).into(),
|
||||||
Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
|
Self::IconButton => cx.build_view(|_| ui::IconButtonStory).into(),
|
||||||
Self::Input => cx.build_view(|_| ui::InputStory).into(),
|
|
||||||
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
|
Self::Keybinding => cx.build_view(|_| ui::KeybindingStory).into(),
|
||||||
Self::Label => cx.build_view(|_| ui::LabelStory).into(),
|
Self::Label => cx.build_view(|_| ui::LabelStory).into(),
|
||||||
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
|
Self::ListItem => cx.build_view(|_| ui::ListItemStory).into(),
|
||||||
|
|
|
@ -86,6 +86,10 @@ impl ThemeRegistry {
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn clear(&mut self) {
|
||||||
|
self.themes.clear();
|
||||||
|
}
|
||||||
|
|
||||||
pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
|
pub fn list_names(&self, _staff: bool) -> impl Iterator<Item = SharedString> + '_ {
|
||||||
self.themes.keys().cloned()
|
self.themes.keys().cloned()
|
||||||
}
|
}
|
||||||
|
|
29
crates/theme_selector2/Cargo.toml
Normal file
29
crates/theme_selector2/Cargo.toml
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
[package]
|
||||||
|
name = "theme_selector2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/theme_selector.rs"
|
||||||
|
doctest = false
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||||
|
fs = { package = "fs2", path = "../fs2" }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
feature_flags = { package = "feature_flags2", path = "../feature_flags2" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
log.workspace = true
|
||||||
|
parking_lot.workspace = true
|
||||||
|
postage.workspace = true
|
||||||
|
smol.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
276
crates/theme_selector2/src/theme_selector.rs
Normal file
276
crates/theme_selector2/src/theme_selector.rs
Normal file
|
@ -0,0 +1,276 @@
|
||||||
|
use feature_flags::FeatureFlagAppExt;
|
||||||
|
use fs::Fs;
|
||||||
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render,
|
||||||
|
SharedString, View, ViewContext, VisualContext, WeakView,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use settings::{update_settings_file, SettingsStore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use theme::{ActiveTheme, Theme, ThemeRegistry, ThemeSettings};
|
||||||
|
use ui::ListItem;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{ui::HighlightedLabel, Workspace};
|
||||||
|
|
||||||
|
actions!(Toggle, Reload);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(
|
||||||
|
|workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
|
||||||
|
workspace.register_action(toggle);
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext<Workspace>) {
|
||||||
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
workspace.toggle_modal(cx, |cx| {
|
||||||
|
ThemeSelector::new(
|
||||||
|
ThemeSelectorDelegate::new(cx.view().downgrade(), fs, cx),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
pub fn reload(cx: &mut AppContext) {
|
||||||
|
let current_theme_name = cx.theme().name.clone();
|
||||||
|
let current_theme = cx.update_global(|registry: &mut ThemeRegistry, _cx| {
|
||||||
|
registry.clear();
|
||||||
|
registry.get(¤t_theme_name)
|
||||||
|
});
|
||||||
|
match current_theme {
|
||||||
|
Ok(theme) => {
|
||||||
|
ThemeSelectorDelegate::set_theme(theme, cx);
|
||||||
|
log::info!("reloaded theme {}", current_theme_name);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("failed to load theme {}: {:?}", current_theme_name, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThemeSelector {
|
||||||
|
picker: View<Picker<ThemeSelectorDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for ThemeSelector {}
|
||||||
|
|
||||||
|
impl FocusableView for ThemeSelector {
|
||||||
|
fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
|
||||||
|
self.picker.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for ThemeSelector {
|
||||||
|
type Element = View<Picker<ThemeSelectorDelegate>>;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeSelector {
|
||||||
|
pub fn new(delegate: ThemeSelectorDelegate, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||||
|
Self { picker }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ThemeSelectorDelegate {
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
theme_names: Vec<SharedString>,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
original_theme: Arc<Theme>,
|
||||||
|
selection_completed: bool,
|
||||||
|
selected_index: usize,
|
||||||
|
view: WeakView<ThemeSelector>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ThemeSelectorDelegate {
|
||||||
|
fn new(
|
||||||
|
weak_view: WeakView<ThemeSelector>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: &mut ViewContext<ThemeSelector>,
|
||||||
|
) -> Self {
|
||||||
|
let original_theme = cx.theme().clone();
|
||||||
|
|
||||||
|
let staff_mode = cx.is_staff();
|
||||||
|
let registry = cx.global::<Arc<ThemeRegistry>>();
|
||||||
|
let theme_names = registry.list(staff_mode).collect::<Vec<_>>();
|
||||||
|
//todo!(theme sorting)
|
||||||
|
// theme_names.sort_unstable_by(|a, b| a.is_light.cmp(&b.is_light).then(a.name.cmp(&b.name)));
|
||||||
|
let matches = theme_names
|
||||||
|
.iter()
|
||||||
|
.map(|meta| StringMatch {
|
||||||
|
candidate_id: 0,
|
||||||
|
score: 0.0,
|
||||||
|
positions: Default::default(),
|
||||||
|
string: meta.to_string(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
let mut this = Self {
|
||||||
|
fs,
|
||||||
|
theme_names,
|
||||||
|
matches,
|
||||||
|
original_theme: original_theme.clone(),
|
||||||
|
selected_index: 0,
|
||||||
|
selection_completed: false,
|
||||||
|
view: weak_view,
|
||||||
|
};
|
||||||
|
this.select_if_matching(&original_theme.name);
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_selected_theme(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
|
||||||
|
if let Some(mat) = self.matches.get(self.selected_index) {
|
||||||
|
let registry = cx.global::<Arc<ThemeRegistry>>();
|
||||||
|
match registry.get(&mat.string) {
|
||||||
|
Ok(theme) => {
|
||||||
|
Self::set_theme(theme, cx);
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
log::error!("error loading theme {}: {}", mat.string, error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn select_if_matching(&mut self, theme_name: &str) {
|
||||||
|
self.selected_index = self
|
||||||
|
.matches
|
||||||
|
.iter()
|
||||||
|
.position(|mat| mat.string == theme_name)
|
||||||
|
.unwrap_or(self.selected_index);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_theme(theme: Arc<Theme>, cx: &mut AppContext) {
|
||||||
|
cx.update_global(|store: &mut SettingsStore, cx| {
|
||||||
|
let mut theme_settings = store.get::<ThemeSettings>(None).clone();
|
||||||
|
theme_settings.active_theme = theme;
|
||||||
|
store.override_global(theme_settings);
|
||||||
|
cx.refresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for ThemeSelectorDelegate {
|
||||||
|
type ListItem = ui::ListItem;
|
||||||
|
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Select Theme...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
|
||||||
|
self.selection_completed = true;
|
||||||
|
|
||||||
|
let theme_name = cx.theme().name.clone();
|
||||||
|
update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings| {
|
||||||
|
settings.theme = Some(theme_name.to_string());
|
||||||
|
});
|
||||||
|
|
||||||
|
self.view
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>) {
|
||||||
|
if !self.selection_completed {
|
||||||
|
Self::set_theme(self.original_theme.clone(), cx);
|
||||||
|
self.selection_completed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
self.show_selected_theme(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
cx: &mut ViewContext<Picker<ThemeSelectorDelegate>>,
|
||||||
|
) -> gpui::Task<()> {
|
||||||
|
let background = cx.background_executor().clone();
|
||||||
|
let candidates = self
|
||||||
|
.theme_names
|
||||||
|
.iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, meta)| StringMatchCandidate {
|
||||||
|
id,
|
||||||
|
char_bag: meta.as_ref().into(),
|
||||||
|
string: meta.to_string(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
background,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, cx| {
|
||||||
|
this.delegate.matches = matches;
|
||||||
|
this.delegate.selected_index = this
|
||||||
|
.delegate
|
||||||
|
.selected_index
|
||||||
|
.min(this.delegate.matches.len().saturating_sub(1));
|
||||||
|
this.delegate.show_selected_theme(cx);
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_cx: &mut ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let theme_match = &self.matches[ix];
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.inset(true)
|
||||||
|
.selected(selected)
|
||||||
|
.child(HighlightedLabel::new(
|
||||||
|
theme_match.string.clone(),
|
||||||
|
theme_match.positions.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
5
crates/ui2/src/clickable.rs
Normal file
5
crates/ui2/src/clickable.rs
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
use gpui::{ClickEvent, WindowContext};
|
||||||
|
|
||||||
|
pub trait Clickable {
|
||||||
|
fn on_click(self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self;
|
||||||
|
}
|
|
@ -1,12 +1,12 @@
|
||||||
mod avatar;
|
mod avatar;
|
||||||
mod button;
|
mod button;
|
||||||
|
mod button2;
|
||||||
mod checkbox;
|
mod checkbox;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod disclosure;
|
mod disclosure;
|
||||||
mod divider;
|
mod divider;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod icon_button;
|
mod icon_button;
|
||||||
mod input;
|
|
||||||
mod keybinding;
|
mod keybinding;
|
||||||
mod label;
|
mod label;
|
||||||
mod list;
|
mod list;
|
||||||
|
@ -21,13 +21,13 @@ mod stories;
|
||||||
|
|
||||||
pub use avatar::*;
|
pub use avatar::*;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
|
pub use button2::*;
|
||||||
pub use checkbox::*;
|
pub use checkbox::*;
|
||||||
pub use context_menu::*;
|
pub use context_menu::*;
|
||||||
pub use disclosure::*;
|
pub use disclosure::*;
|
||||||
pub use divider::*;
|
pub use divider::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use icon_button::*;
|
pub use icon_button::*;
|
||||||
pub use input::*;
|
|
||||||
pub use keybinding::*;
|
pub use keybinding::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use list::*;
|
pub use list::*;
|
||||||
|
|
|
@ -1,9 +1,7 @@
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use gpui::{
|
use gpui::{
|
||||||
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
|
ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
|
||||||
StatefulInteractiveElement, WindowContext,
|
|
||||||
};
|
};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
|
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
|
||||||
|
@ -67,7 +65,7 @@ impl ButtonVariant {
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct Button {
|
pub struct Button {
|
||||||
disabled: bool,
|
disabled: bool,
|
||||||
click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
|
click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||||
icon: Option<Icon>,
|
icon: Option<Icon>,
|
||||||
icon_position: Option<IconPosition>,
|
icon_position: Option<IconPosition>,
|
||||||
label: SharedString,
|
label: SharedString,
|
||||||
|
@ -118,7 +116,7 @@ impl RenderOnce for Button {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(click_handler) = self.click_handler.clone() {
|
if let Some(click_handler) = self.click_handler.clone() {
|
||||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
button = button.on_click(move |event, cx| {
|
||||||
click_handler(event, cx);
|
click_handler(event, cx);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -168,10 +166,7 @@ impl Button {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||||
mut self,
|
|
||||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
|
||||||
) -> Self {
|
|
||||||
self.click_handler = Some(Rc::new(handler));
|
self.click_handler = Some(Rc::new(handler));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
413
crates/ui2/src/components/button2.rs
Normal file
413
crates/ui2/src/components/button2.rs
Normal file
|
@ -0,0 +1,413 @@
|
||||||
|
use gpui::{
|
||||||
|
rems, AnyElement, AnyView, ClickEvent, Div, Hsla, IntoElement, Rems, Stateful,
|
||||||
|
StatefulInteractiveElement, WindowContext,
|
||||||
|
};
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
use crate::{h_stack, prelude::*};
|
||||||
|
|
||||||
|
// 🚧 Heavily WIP 🚧
|
||||||
|
|
||||||
|
// #[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
// pub enum ButtonType2 {
|
||||||
|
// #[default]
|
||||||
|
// DefaultButton,
|
||||||
|
// IconButton,
|
||||||
|
// ButtonLike,
|
||||||
|
// SplitButton,
|
||||||
|
// ToggleButton,
|
||||||
|
// }
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum IconPosition2 {
|
||||||
|
#[default]
|
||||||
|
Before,
|
||||||
|
After,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum ButtonStyle2 {
|
||||||
|
#[default]
|
||||||
|
Filled,
|
||||||
|
// Tinted,
|
||||||
|
Subtle,
|
||||||
|
Transparent,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub struct ButtonStyle {
|
||||||
|
pub background: Hsla,
|
||||||
|
pub border_color: Hsla,
|
||||||
|
pub label_color: Hsla,
|
||||||
|
pub icon_color: Hsla,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonStyle2 {
|
||||||
|
pub fn enabled(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||||
|
match self {
|
||||||
|
ButtonStyle2::Filled => ButtonStyle {
|
||||||
|
background: cx.theme().colors().element_background,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Subtle => ButtonStyle {
|
||||||
|
background: cx.theme().colors().ghost_element_background,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Transparent => ButtonStyle {
|
||||||
|
background: gpui::transparent_black(),
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn hovered(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||||
|
match self {
|
||||||
|
ButtonStyle2::Filled => ButtonStyle {
|
||||||
|
background: cx.theme().colors().element_hover,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Subtle => ButtonStyle {
|
||||||
|
background: cx.theme().colors().ghost_element_hover,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Transparent => ButtonStyle {
|
||||||
|
background: gpui::transparent_black(),
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
// TODO: These are not great
|
||||||
|
label_color: Color::Muted.color(cx),
|
||||||
|
// TODO: These are not great
|
||||||
|
icon_color: Color::Muted.color(cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn active(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||||
|
match self {
|
||||||
|
ButtonStyle2::Filled => ButtonStyle {
|
||||||
|
background: cx.theme().colors().element_active,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Subtle => ButtonStyle {
|
||||||
|
background: cx.theme().colors().ghost_element_active,
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Transparent => ButtonStyle {
|
||||||
|
background: gpui::transparent_black(),
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
// TODO: These are not great
|
||||||
|
label_color: Color::Muted.color(cx),
|
||||||
|
// TODO: These are not great
|
||||||
|
icon_color: Color::Muted.color(cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn focused(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||||
|
match self {
|
||||||
|
ButtonStyle2::Filled => ButtonStyle {
|
||||||
|
background: cx.theme().colors().element_background,
|
||||||
|
border_color: cx.theme().colors().border_focused,
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Subtle => ButtonStyle {
|
||||||
|
background: cx.theme().colors().ghost_element_background,
|
||||||
|
border_color: cx.theme().colors().border_focused,
|
||||||
|
label_color: Color::Default.color(cx),
|
||||||
|
icon_color: Color::Default.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Transparent => ButtonStyle {
|
||||||
|
background: gpui::transparent_black(),
|
||||||
|
border_color: cx.theme().colors().border_focused,
|
||||||
|
label_color: Color::Accent.color(cx),
|
||||||
|
icon_color: Color::Accent.color(cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disabled(self, cx: &mut WindowContext) -> ButtonStyle {
|
||||||
|
match self {
|
||||||
|
ButtonStyle2::Filled => ButtonStyle {
|
||||||
|
background: cx.theme().colors().element_disabled,
|
||||||
|
border_color: cx.theme().colors().border_disabled,
|
||||||
|
label_color: Color::Disabled.color(cx),
|
||||||
|
icon_color: Color::Disabled.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Subtle => ButtonStyle {
|
||||||
|
background: cx.theme().colors().ghost_element_disabled,
|
||||||
|
border_color: cx.theme().colors().border_disabled,
|
||||||
|
label_color: Color::Disabled.color(cx),
|
||||||
|
icon_color: Color::Disabled.color(cx),
|
||||||
|
},
|
||||||
|
ButtonStyle2::Transparent => ButtonStyle {
|
||||||
|
background: gpui::transparent_black(),
|
||||||
|
border_color: gpui::transparent_black(),
|
||||||
|
label_color: Color::Disabled.color(cx),
|
||||||
|
icon_color: Color::Disabled.color(cx),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, PartialEq, Clone, Copy)]
|
||||||
|
pub enum ButtonSize2 {
|
||||||
|
#[default]
|
||||||
|
Default,
|
||||||
|
Compact,
|
||||||
|
None,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonSize2 {
|
||||||
|
fn height(self) -> Rems {
|
||||||
|
match self {
|
||||||
|
ButtonSize2::Default => rems(22. / 16.),
|
||||||
|
ButtonSize2::Compact => rems(18. / 16.),
|
||||||
|
ButtonSize2::None => rems(16. / 16.),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct Button {
|
||||||
|
// id: ElementId,
|
||||||
|
// icon: Option<Icon>,
|
||||||
|
// icon_color: Option<Color>,
|
||||||
|
// icon_position: Option<IconPosition2>,
|
||||||
|
// label: Option<Label>,
|
||||||
|
// label_color: Option<Color>,
|
||||||
|
// appearance: ButtonAppearance2,
|
||||||
|
// state: InteractionState,
|
||||||
|
// selected: bool,
|
||||||
|
// disabled: bool,
|
||||||
|
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||||
|
// width: Option<DefiniteLength>,
|
||||||
|
// action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||||
|
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||||
|
// /// Used to pass down some content to the button
|
||||||
|
// /// to enable creating custom buttons.
|
||||||
|
// children: SmallVec<[AnyElement; 2]>,
|
||||||
|
// }
|
||||||
|
|
||||||
|
pub trait ButtonCommon: Clickable {
|
||||||
|
fn id(&self) -> &ElementId;
|
||||||
|
fn style(self, style: ButtonStyle2) -> Self;
|
||||||
|
fn disabled(self, disabled: bool) -> Self;
|
||||||
|
fn size(self, size: ButtonSize2) -> Self;
|
||||||
|
fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
|
||||||
|
// fn width(&mut self, width: DefiniteLength) -> &mut Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct LabelButton {
|
||||||
|
// // Base properties...
|
||||||
|
// id: ElementId,
|
||||||
|
// appearance: ButtonAppearance,
|
||||||
|
// state: InteractionState,
|
||||||
|
// disabled: bool,
|
||||||
|
// size: ButtonSize,
|
||||||
|
// tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||||
|
// width: Option<DefiniteLength>,
|
||||||
|
// // Button-specific properties...
|
||||||
|
// label: Option<SharedString>,
|
||||||
|
// label_color: Option<Color>,
|
||||||
|
// icon: Option<Icon>,
|
||||||
|
// icon_color: Option<Color>,
|
||||||
|
// icon_position: Option<IconPosition>,
|
||||||
|
// // Define more fields for additional properties as needed
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ButtonCommon for LabelButton {
|
||||||
|
// fn id(&self) -> &ElementId {
|
||||||
|
// &self.id
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn appearance(&mut self, appearance: ButtonAppearance) -> &mut Self {
|
||||||
|
// self.style= style;
|
||||||
|
// self
|
||||||
|
// }
|
||||||
|
// // implement methods from ButtonCommon trait...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl LabelButton {
|
||||||
|
// pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||||
|
// Self {
|
||||||
|
// id: id.into(),
|
||||||
|
// label: Some(label.into()),
|
||||||
|
// // initialize other fields with default values...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ... Define other builder methods specific to Button type...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// TODO: Icon Button
|
||||||
|
|
||||||
|
#[derive(IntoElement)]
|
||||||
|
pub struct ButtonLike {
|
||||||
|
id: ElementId,
|
||||||
|
style: ButtonStyle2,
|
||||||
|
disabled: bool,
|
||||||
|
size: ButtonSize2,
|
||||||
|
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
|
||||||
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ButtonLike {
|
||||||
|
pub fn children(
|
||||||
|
&mut self,
|
||||||
|
children: impl IntoIterator<Item = impl Into<AnyElement>>,
|
||||||
|
) -> &mut Self {
|
||||||
|
self.children = children.into_iter().map(Into::into).collect();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
Self {
|
||||||
|
id: id.into(),
|
||||||
|
style: ButtonStyle2::default(),
|
||||||
|
disabled: false,
|
||||||
|
size: ButtonSize2::Default,
|
||||||
|
tooltip: None,
|
||||||
|
children: SmallVec::new(),
|
||||||
|
on_click: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clickable for ButtonLike {
|
||||||
|
fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||||
|
self.on_click = Some(Box::new(handler));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// impl Selectable for ButtonLike {
|
||||||
|
// fn selected(&mut self, selected: bool) -> &mut Self {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// fn selected_tooltip(
|
||||||
|
// &mut self,
|
||||||
|
// tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
|
||||||
|
// ) -> &mut Self {
|
||||||
|
// todo!()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
impl ButtonCommon for ButtonLike {
|
||||||
|
fn id(&self) -> &ElementId {
|
||||||
|
&self.id
|
||||||
|
}
|
||||||
|
|
||||||
|
fn style(mut self, style: ButtonStyle2) -> Self {
|
||||||
|
self.style = style;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn disabled(mut self, disabled: bool) -> Self {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn size(mut self, size: ButtonSize2) -> Self {
|
||||||
|
self.size = size;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||||
|
self.tooltip = Some(Box::new(tooltip));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RenderOnce for ButtonLike {
|
||||||
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
|
h_stack()
|
||||||
|
.id(self.id.clone())
|
||||||
|
.h(self.size.height())
|
||||||
|
.rounded_md()
|
||||||
|
.cursor_pointer()
|
||||||
|
.gap_1()
|
||||||
|
.px_1()
|
||||||
|
.bg(self.style.enabled(cx).background)
|
||||||
|
.hover(|hover| hover.bg(self.style.hovered(cx).background))
|
||||||
|
.active(|active| active.bg(self.style.active(cx).background))
|
||||||
|
.when_some(
|
||||||
|
self.on_click.filter(|_| !self.disabled),
|
||||||
|
|this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)),
|
||||||
|
)
|
||||||
|
.when_some(self.tooltip, |this, tooltip| {
|
||||||
|
this.tooltip(move |cx| tooltip(cx))
|
||||||
|
})
|
||||||
|
.children(self.children)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ParentElement for ButtonLike {
|
||||||
|
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||||
|
&mut self.children
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// pub struct ToggleButton {
|
||||||
|
// // based on either IconButton2 or Button, with additional 'selected: bool' property
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ButtonCommon for ToggleButton {
|
||||||
|
// fn id(&self) -> &ElementId {
|
||||||
|
// &self.id
|
||||||
|
// }
|
||||||
|
// // ... Implement other methods from ButtonCommon trait with builder patterns...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ToggleButton {
|
||||||
|
// pub fn new() -> Self {
|
||||||
|
// // Initialize with default values
|
||||||
|
// Self {
|
||||||
|
// // ... initialize fields, possibly with defaults or required parameters...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ... Define other builder methods specific to ToggleButton type...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub struct SplitButton {
|
||||||
|
// // Base properties...
|
||||||
|
// id: ElementId,
|
||||||
|
// // Button-specific properties, possibly including a DefaultButton
|
||||||
|
// secondary_action: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
|
||||||
|
// // More fields as necessary...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl ButtonCommon for SplitButton {
|
||||||
|
// fn id(&self) -> &ElementId {
|
||||||
|
// &self.id
|
||||||
|
// }
|
||||||
|
// // ... Implement other methods from ButtonCommon trait with builder patterns...
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl SplitButton {
|
||||||
|
// pub fn new(id: impl Into<ElementId>) -> Self {
|
||||||
|
// Self {
|
||||||
|
// id: id.into(),
|
||||||
|
// // ... initialize other fields with default values...
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // ... Define other builder methods specific to SplitButton type...
|
||||||
|
// }
|
|
@ -1,19 +1,30 @@
|
||||||
use gpui::{div, Element, ParentElement};
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{Color, Icon, IconElement, IconSize, Toggle};
|
use gpui::{div, ClickEvent, Element, IntoElement, ParentElement, WindowContext};
|
||||||
|
|
||||||
pub fn disclosure_control(toggle: Toggle) -> impl Element {
|
use crate::{Color, Icon, IconButton, IconSize, Toggle};
|
||||||
|
|
||||||
|
pub fn disclosure_control(
|
||||||
|
toggle: Toggle,
|
||||||
|
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
) -> impl Element {
|
||||||
match (toggle.is_toggleable(), toggle.is_toggled()) {
|
match (toggle.is_toggleable(), toggle.is_toggled()) {
|
||||||
(false, _) => div(),
|
(false, _) => div(),
|
||||||
(_, true) => div().child(
|
(_, true) => div().child(
|
||||||
IconElement::new(Icon::ChevronDown)
|
IconButton::new("toggle", Icon::ChevronDown)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small)
|
||||||
|
.when_some(on_toggle, move |el, on_toggle| {
|
||||||
|
el.on_click(move |e, cx| on_toggle(e, cx))
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
(_, false) => div().child(
|
(_, false) => div().child(
|
||||||
IconElement::new(Icon::ChevronRight)
|
IconButton::new("toggle", Icon::ChevronRight)
|
||||||
.color(Color::Muted)
|
.color(Color::Muted)
|
||||||
.size(IconSize::Small),
|
.size(IconSize::Small)
|
||||||
|
.when_some(on_toggle, move |el, on_toggle| {
|
||||||
|
el.on_click(move |e, cx| on_toggle(e, cx))
|
||||||
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,25 +1,26 @@
|
||||||
use crate::{h_stack, prelude::*, Icon, IconElement};
|
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
|
||||||
use gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
|
use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
pub struct IconButton {
|
pub struct IconButton {
|
||||||
id: ElementId,
|
id: ElementId,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
color: Color,
|
color: Color,
|
||||||
|
size: IconSize,
|
||||||
variant: ButtonVariant,
|
variant: ButtonVariant,
|
||||||
state: InteractionState,
|
disabled: bool,
|
||||||
selected: bool,
|
selected: bool,
|
||||||
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
|
tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>>,
|
||||||
on_mouse_down: Option<Box<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for IconButton {
|
impl RenderOnce for IconButton {
|
||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let icon_color = match (self.state, self.color) {
|
let icon_color = match (self.disabled, self.selected, self.color) {
|
||||||
(InteractionState::Disabled, _) => Color::Disabled,
|
(true, _, _) => Color::Disabled,
|
||||||
(InteractionState::Active, _) => Color::Selected,
|
(false, true, _) => Color::Selected,
|
||||||
_ => self.color,
|
_ => self.color,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -50,10 +51,14 @@ impl RenderOnce for IconButton {
|
||||||
// place we use an icon button.
|
// place we use an icon button.
|
||||||
// .hover(|style| style.bg(bg_hover_color))
|
// .hover(|style| style.bg(bg_hover_color))
|
||||||
.active(|style| style.bg(bg_active_color))
|
.active(|style| style.bg(bg_active_color))
|
||||||
.child(IconElement::new(self.icon).color(icon_color));
|
.child(
|
||||||
|
IconElement::new(self.icon)
|
||||||
|
.size(self.size)
|
||||||
|
.color(icon_color),
|
||||||
|
);
|
||||||
|
|
||||||
if let Some(click_handler) = self.on_mouse_down {
|
if let Some(click_handler) = self.on_click {
|
||||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
button = button.on_click(move |event, cx| {
|
||||||
cx.stop_propagation();
|
cx.stop_propagation();
|
||||||
click_handler(event, cx);
|
click_handler(event, cx);
|
||||||
})
|
})
|
||||||
|
@ -65,8 +70,7 @@ impl RenderOnce for IconButton {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// HACK: Add an additional identified element wrapper to fix tooltips not showing up.
|
button
|
||||||
div().id(self.id.clone()).child(button)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -76,11 +80,12 @@ impl IconButton {
|
||||||
id: id.into(),
|
id: id.into(),
|
||||||
icon,
|
icon,
|
||||||
color: Color::default(),
|
color: Color::default(),
|
||||||
|
size: Default::default(),
|
||||||
variant: ButtonVariant::default(),
|
variant: ButtonVariant::default(),
|
||||||
state: InteractionState::default(),
|
|
||||||
selected: false,
|
selected: false,
|
||||||
|
disabled: false,
|
||||||
tooltip: None,
|
tooltip: None,
|
||||||
on_mouse_down: None,
|
on_click: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -94,13 +99,13 @@ impl IconButton {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
pub fn size(mut self, size: IconSize) -> Self {
|
||||||
self.variant = variant;
|
self.size = size;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn state(mut self, state: InteractionState) -> Self {
|
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||||
self.state = state;
|
self.variant = variant;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -109,16 +114,18 @@ impl IconButton {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||||
|
self.disabled = disabled;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
pub fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
|
||||||
self.tooltip = Some(Box::new(tooltip));
|
self.tooltip = Some(Box::new(tooltip));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_click(
|
pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
|
||||||
mut self,
|
self.on_click = Some(Box::new(handler));
|
||||||
handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
|
|
||||||
) -> Self {
|
|
||||||
self.on_mouse_down = Some(Box::new(handler));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,108 +0,0 @@
|
||||||
use crate::{prelude::*, Label};
|
|
||||||
use gpui::{prelude::*, Div, IntoElement, Stateful};
|
|
||||||
|
|
||||||
#[derive(Default, PartialEq)]
|
|
||||||
pub enum InputVariant {
|
|
||||||
#[default]
|
|
||||||
Ghost,
|
|
||||||
Filled,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
|
||||||
pub struct Input {
|
|
||||||
placeholder: SharedString,
|
|
||||||
value: String,
|
|
||||||
state: InteractionState,
|
|
||||||
variant: InputVariant,
|
|
||||||
disabled: bool,
|
|
||||||
is_active: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RenderOnce for Input {
|
|
||||||
type Rendered = Stateful<Div>;
|
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
|
||||||
let (input_bg, input_hover_bg, input_active_bg) = match self.variant {
|
|
||||||
InputVariant::Ghost => (
|
|
||||||
cx.theme().colors().ghost_element_background,
|
|
||||||
cx.theme().colors().ghost_element_hover,
|
|
||||||
cx.theme().colors().ghost_element_active,
|
|
||||||
),
|
|
||||||
InputVariant::Filled => (
|
|
||||||
cx.theme().colors().element_background,
|
|
||||||
cx.theme().colors().element_hover,
|
|
||||||
cx.theme().colors().element_active,
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
let placeholder_label = Label::new(self.placeholder.clone()).color(if self.disabled {
|
|
||||||
Color::Disabled
|
|
||||||
} else {
|
|
||||||
Color::Placeholder
|
|
||||||
});
|
|
||||||
|
|
||||||
let label = Label::new(self.value.clone()).color(if self.disabled {
|
|
||||||
Color::Disabled
|
|
||||||
} else {
|
|
||||||
Color::Default
|
|
||||||
});
|
|
||||||
|
|
||||||
div()
|
|
||||||
.id("input")
|
|
||||||
.h_7()
|
|
||||||
.w_full()
|
|
||||||
.px_2()
|
|
||||||
.border()
|
|
||||||
.border_color(cx.theme().styles.system.transparent)
|
|
||||||
.bg(input_bg)
|
|
||||||
.hover(|style| style.bg(input_hover_bg))
|
|
||||||
.active(|style| style.bg(input_active_bg))
|
|
||||||
.flex()
|
|
||||||
.items_center()
|
|
||||||
.child(div().flex().items_center().text_ui_sm().map(move |this| {
|
|
||||||
if self.value.is_empty() {
|
|
||||||
this.child(placeholder_label)
|
|
||||||
} else {
|
|
||||||
this.child(label)
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Input {
|
|
||||||
pub fn new(placeholder: impl Into<SharedString>) -> Self {
|
|
||||||
Self {
|
|
||||||
placeholder: placeholder.into(),
|
|
||||||
value: "".to_string(),
|
|
||||||
state: InteractionState::default(),
|
|
||||||
variant: InputVariant::default(),
|
|
||||||
disabled: false,
|
|
||||||
is_active: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn value(mut self, value: String) -> Self {
|
|
||||||
self.value = value;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn state(mut self, state: InteractionState) -> Self {
|
|
||||||
self.state = state;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn variant(mut self, variant: InputVariant) -> Self {
|
|
||||||
self.variant = variant;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
||||||
self.disabled = disabled;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_active(mut self, is_active: bool) -> Self {
|
|
||||||
self.is_active = is_active;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,9 @@ pub struct ListHeader {
|
||||||
left_icon: Option<Icon>,
|
left_icon: Option<Icon>,
|
||||||
meta: Option<ListHeaderMeta>,
|
meta: Option<ListHeaderMeta>,
|
||||||
toggle: Toggle,
|
toggle: Toggle,
|
||||||
|
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
|
selected: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ListHeader {
|
impl ListHeader {
|
||||||
|
@ -36,6 +38,8 @@ impl ListHeader {
|
||||||
meta: None,
|
meta: None,
|
||||||
inset: false,
|
inset: false,
|
||||||
toggle: Toggle::NotToggleable,
|
toggle: Toggle::NotToggleable,
|
||||||
|
on_toggle: None,
|
||||||
|
selected: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +48,14 @@ impl ListHeader {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_toggle(
|
||||||
|
mut self,
|
||||||
|
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_toggle = Some(Rc::new(on_toggle));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||||
self.left_icon = left_icon;
|
self.left_icon = left_icon;
|
||||||
self
|
self
|
||||||
|
@ -57,13 +69,18 @@ impl ListHeader {
|
||||||
self.meta = meta;
|
self.meta = meta;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
|
self.selected = selected;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RenderOnce for ListHeader {
|
impl RenderOnce for ListHeader {
|
||||||
type Rendered = Div;
|
type Rendered = Div;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let disclosure_control = disclosure_control(self.toggle);
|
let disclosure_control = disclosure_control(self.toggle, self.on_toggle);
|
||||||
|
|
||||||
let meta = match self.meta {
|
let meta = match self.meta {
|
||||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||||
|
@ -85,6 +102,9 @@ impl RenderOnce for ListHeader {
|
||||||
div()
|
div()
|
||||||
.h_5()
|
.h_5()
|
||||||
.when(self.inset, |this| this.px_2())
|
.when(self.inset, |this| this.px_2())
|
||||||
|
.when(self.selected, |this| {
|
||||||
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
|
})
|
||||||
.flex()
|
.flex()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
|
@ -177,6 +197,7 @@ pub struct ListItem {
|
||||||
toggle: Toggle,
|
toggle: Toggle,
|
||||||
inset: bool,
|
inset: bool,
|
||||||
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
|
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||||
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
|
||||||
children: SmallVec<[AnyElement; 2]>,
|
children: SmallVec<[AnyElement; 2]>,
|
||||||
}
|
}
|
||||||
|
@ -193,6 +214,7 @@ impl ListItem {
|
||||||
inset: false,
|
inset: false,
|
||||||
on_click: None,
|
on_click: None,
|
||||||
on_secondary_mouse_down: None,
|
on_secondary_mouse_down: None,
|
||||||
|
on_toggle: None,
|
||||||
children: SmallVec::new(),
|
children: SmallVec::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -230,6 +252,14 @@ impl ListItem {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn on_toggle(
|
||||||
|
mut self,
|
||||||
|
on_toggle: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.on_toggle = Some(Rc::new(on_toggle));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn selected(mut self, selected: bool) -> Self {
|
pub fn selected(mut self, selected: bool) -> Self {
|
||||||
self.selected = selected;
|
self.selected = selected;
|
||||||
self
|
self
|
||||||
|
@ -255,19 +285,6 @@ impl RenderOnce for ListItem {
|
||||||
type Rendered = Stateful<Div>;
|
type Rendered = Stateful<Div>;
|
||||||
|
|
||||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||||
let left_content = match self.left_slot.clone() {
|
|
||||||
Some(GraphicSlot::Icon(i)) => Some(
|
|
||||||
h_stack().child(
|
|
||||||
IconElement::new(i)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
|
|
||||||
Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
div()
|
div()
|
||||||
.id(self.id)
|
.id(self.id)
|
||||||
.relative()
|
.relative()
|
||||||
|
@ -282,8 +299,8 @@ impl RenderOnce for ListItem {
|
||||||
.when(self.selected, |this| {
|
.when(self.selected, |this| {
|
||||||
this.bg(cx.theme().colors().ghost_element_selected)
|
this.bg(cx.theme().colors().ghost_element_selected)
|
||||||
})
|
})
|
||||||
.when_some(self.on_click.clone(), |this, on_click| {
|
.when_some(self.on_click, |this, on_click| {
|
||||||
this.on_click(move |event, cx| {
|
this.cursor_pointer().on_click(move |event, cx| {
|
||||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||||
// but we only care about the left button.
|
// but we only care about the left button.
|
||||||
if event.down.button == MouseButton::Left {
|
if event.down.button == MouseButton::Left {
|
||||||
|
@ -304,23 +321,18 @@ impl RenderOnce for ListItem {
|
||||||
.gap_1()
|
.gap_1()
|
||||||
.items_center()
|
.items_center()
|
||||||
.relative()
|
.relative()
|
||||||
.child(disclosure_control(self.toggle))
|
.child(disclosure_control(self.toggle, self.on_toggle))
|
||||||
.children(left_content)
|
.map(|this| match self.left_slot {
|
||||||
.children(self.children)
|
Some(GraphicSlot::Icon(i)) => this.child(
|
||||||
// HACK: We need to attach the `on_click` handler to the child element in order to have the click
|
IconElement::new(i)
|
||||||
// event actually fire.
|
.size(IconSize::Small)
|
||||||
// Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
|
.color(Color::Muted),
|
||||||
// outer `div`.
|
),
|
||||||
.id("on_click_hack")
|
Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)),
|
||||||
.when_some(self.on_click, |this, on_click| {
|
Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)),
|
||||||
this.on_click(move |event, cx| {
|
None => this,
|
||||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
|
||||||
// but we only care about the left button.
|
|
||||||
if event.down.button == MouseButton::Left {
|
|
||||||
(on_click)(event, cx)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
}),
|
.children(self.children),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,18 +4,15 @@ mod checkbox;
|
||||||
mod context_menu;
|
mod context_menu;
|
||||||
mod icon;
|
mod icon;
|
||||||
mod icon_button;
|
mod icon_button;
|
||||||
mod input;
|
|
||||||
mod keybinding;
|
mod keybinding;
|
||||||
mod label;
|
mod label;
|
||||||
mod list_item;
|
mod list_item;
|
||||||
|
|
||||||
pub use avatar::*;
|
pub use avatar::*;
|
||||||
pub use button::*;
|
pub use button::*;
|
||||||
pub use checkbox::*;
|
pub use checkbox::*;
|
||||||
pub use context_menu::*;
|
pub use context_menu::*;
|
||||||
pub use icon::*;
|
pub use icon::*;
|
||||||
pub use icon_button::*;
|
pub use icon_button::*;
|
||||||
pub use input::*;
|
|
||||||
pub use keybinding::*;
|
pub use keybinding::*;
|
||||||
pub use label::*;
|
pub use label::*;
|
||||||
pub use list_item::*;
|
pub use list_item::*;
|
||||||
|
|
|
@ -1,9 +1,8 @@
|
||||||
use gpui::{rems, Div, Render};
|
use gpui::{Div, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
use strum::IntoEnumIterator;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
|
use crate::{h_stack, Button, Icon, IconPosition};
|
||||||
|
|
||||||
pub struct ButtonStory;
|
pub struct ButtonStory;
|
||||||
|
|
||||||
|
@ -11,8 +10,6 @@ impl Render for ButtonStory {
|
||||||
type Element = Div;
|
type Element = Div;
|
||||||
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
let states = InteractionState::iter();
|
|
||||||
|
|
||||||
Story::container()
|
Story::container()
|
||||||
.child(Story::title_for::<Button>())
|
.child(Story::title_for::<Button>())
|
||||||
.child(
|
.child(
|
||||||
|
@ -20,119 +17,54 @@ impl Render for ButtonStory {
|
||||||
.flex()
|
.flex()
|
||||||
.gap_8()
|
.gap_8()
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().child(Story::label("Ghost (Default)")).child(
|
||||||
.child(Story::label("Ghost (Default)"))
|
h_stack()
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
.gap_2()
|
||||||
v_stack()
|
.child(Button::new("Label").variant(ButtonVariant::Ghost)),
|
||||||
.gap_1()
|
),
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
Button::new("Label").variant(ButtonVariant::Ghost), // .state(state),
|
|
||||||
)
|
)
|
||||||
})))
|
|
||||||
.child(Story::label("Ghost – Left Icon"))
|
.child(Story::label("Ghost – Left Icon"))
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
.child(
|
||||||
|
h_stack().gap_2().child(
|
||||||
Button::new("Label")
|
Button::new("Label")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.icon(Icon::Plus)
|
.icon(Icon::Plus)
|
||||||
.icon_position(IconPosition::Left), // .state(state),
|
.icon_position(IconPosition::Left),
|
||||||
|
),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})))
|
|
||||||
.child(Story::label("Ghost – Right Icon"))
|
.child(Story::label("Ghost – Right Icon"))
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
.child(
|
||||||
|
h_stack().gap_2().child(
|
||||||
Button::new("Label")
|
Button::new("Label")
|
||||||
.variant(ButtonVariant::Ghost)
|
.variant(ButtonVariant::Ghost)
|
||||||
.icon(Icon::Plus)
|
.icon(Icon::Plus)
|
||||||
.icon_position(IconPosition::Right), // .state(state),
|
.icon_position(IconPosition::Right),
|
||||||
)
|
),
|
||||||
}))),
|
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div().child(Story::label("Filled")).child(
|
||||||
.child(Story::label("Filled"))
|
h_stack()
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
.gap_2()
|
||||||
v_stack()
|
.child(Button::new("Label").variant(ButtonVariant::Filled)),
|
||||||
.gap_1()
|
),
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
Button::new("Label").variant(ButtonVariant::Filled), // .state(state),
|
|
||||||
)
|
)
|
||||||
})))
|
|
||||||
.child(Story::label("Filled – Left Button"))
|
.child(Story::label("Filled – Left Button"))
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
.child(
|
||||||
|
h_stack().gap_2().child(
|
||||||
Button::new("Label")
|
Button::new("Label")
|
||||||
.variant(ButtonVariant::Filled)
|
.variant(ButtonVariant::Filled)
|
||||||
.icon(Icon::Plus)
|
.icon(Icon::Plus)
|
||||||
.icon_position(IconPosition::Left), // .state(state),
|
.icon_position(IconPosition::Left),
|
||||||
|
),
|
||||||
)
|
)
|
||||||
})))
|
|
||||||
.child(Story::label("Filled – Right Button"))
|
.child(Story::label("Filled – Right Button"))
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
.child(
|
||||||
|
h_stack().gap_2().child(
|
||||||
Button::new("Label")
|
Button::new("Label")
|
||||||
.variant(ButtonVariant::Filled)
|
.variant(ButtonVariant::Filled)
|
||||||
.icon(Icon::Plus)
|
.icon(Icon::Plus)
|
||||||
.icon_position(IconPosition::Right), // .state(state),
|
.icon_position(IconPosition::Right),
|
||||||
)
|
|
||||||
}))),
|
|
||||||
)
|
|
||||||
.child(
|
|
||||||
div()
|
|
||||||
.child(Story::label("Fixed With"))
|
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
Button::new("Label")
|
|
||||||
.variant(ButtonVariant::Filled)
|
|
||||||
// .state(state)
|
|
||||||
.width(Some(rems(6.).into())),
|
|
||||||
)
|
|
||||||
})))
|
|
||||||
.child(Story::label("Fixed With – Left Icon"))
|
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
Button::new("Label")
|
|
||||||
.variant(ButtonVariant::Filled)
|
|
||||||
// .state(state)
|
|
||||||
.icon(Icon::Plus)
|
|
||||||
.icon_position(IconPosition::Left)
|
|
||||||
.width(Some(rems(6.).into())),
|
|
||||||
)
|
|
||||||
})))
|
|
||||||
.child(Story::label("Fixed With – Right Icon"))
|
|
||||||
.child(h_stack().gap_2().children(states.clone().map(|state| {
|
|
||||||
v_stack()
|
|
||||||
.gap_1()
|
|
||||||
.child(Label::new(state.to_string()).color(Color::Muted))
|
|
||||||
.child(
|
|
||||||
Button::new("Label")
|
|
||||||
.variant(ButtonVariant::Filled)
|
|
||||||
// .state(state)
|
|
||||||
.icon(Icon::Plus)
|
|
||||||
.icon_position(IconPosition::Right)
|
|
||||||
.width(Some(rems(6.).into())),
|
|
||||||
)
|
|
||||||
}))),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(Story::label("Button with `on_click`"))
|
.child(Story::label("Button with `on_click`"))
|
||||||
|
|
|
@ -1,18 +0,0 @@
|
||||||
use gpui::{Div, Render};
|
|
||||||
use story::Story;
|
|
||||||
|
|
||||||
use crate::prelude::*;
|
|
||||||
use crate::Input;
|
|
||||||
|
|
||||||
pub struct InputStory;
|
|
||||||
|
|
||||||
impl Render for InputStory {
|
|
||||||
type Element = Div;
|
|
||||||
|
|
||||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
|
||||||
Story::container()
|
|
||||||
.child(Story::title_for::<Input>())
|
|
||||||
.child(Story::label("Default"))
|
|
||||||
.child(div().flex().child(Input::new("Search")))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -2,7 +2,7 @@ use gpui::{Div, Render};
|
||||||
use story::Story;
|
use story::Story;
|
||||||
|
|
||||||
use crate::prelude::*;
|
use crate::prelude::*;
|
||||||
use crate::ListItem;
|
use crate::{Icon, ListItem};
|
||||||
|
|
||||||
pub struct ListItemStory;
|
pub struct ListItemStory;
|
||||||
|
|
||||||
|
@ -14,6 +14,20 @@ impl Render for ListItemStory {
|
||||||
.child(Story::title_for::<ListItem>())
|
.child(Story::title_for::<ListItem>())
|
||||||
.child(Story::label("Default"))
|
.child(Story::label("Default"))
|
||||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||||
|
.child(Story::label("With left icon"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_left_icon")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.left_icon(Icon::Bell),
|
||||||
|
)
|
||||||
|
.child(Story::label("With left avatar"))
|
||||||
|
.child(
|
||||||
|
ListItem::new("with_left_avatar")
|
||||||
|
.child("Hello, world!")
|
||||||
|
.left_avatar(SharedString::from(
|
||||||
|
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||||
|
)),
|
||||||
|
)
|
||||||
.child(Story::label("With `on_click`"))
|
.child(Story::label("With `on_click`"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_on_click")
|
ListItem::new("with_on_click")
|
||||||
|
@ -24,11 +38,11 @@ impl Render for ListItemStory {
|
||||||
)
|
)
|
||||||
.child(Story::label("With `on_secondary_mouse_down`"))
|
.child(Story::label("With `on_secondary_mouse_down`"))
|
||||||
.child(
|
.child(
|
||||||
ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
|
ListItem::new("with_on_secondary_mouse_down")
|
||||||
|_event, _cx| {
|
.child("Right click me")
|
||||||
|
.on_secondary_mouse_down(|_event, _cx| {
|
||||||
println!("Right mouse down!");
|
println!("Right mouse down!");
|
||||||
},
|
}),
|
||||||
),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
6
crates/ui2/src/fixed.rs
Normal file
6
crates/ui2/src/fixed.rs
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
use gpui::DefiniteLength;
|
||||||
|
|
||||||
|
pub trait FixedWidth {
|
||||||
|
fn width(self, width: DefiniteLength) -> Self;
|
||||||
|
fn full_width(self) -> Self;
|
||||||
|
}
|
|
@ -3,62 +3,9 @@ pub use gpui::{
|
||||||
ViewContext, WindowContext,
|
ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub use crate::clickable::*;
|
||||||
|
pub use crate::fixed::*;
|
||||||
|
pub use crate::selectable::*;
|
||||||
pub use crate::StyledExt;
|
pub use crate::StyledExt;
|
||||||
pub use crate::{ButtonVariant, Color};
|
pub use crate::{ButtonVariant, Color};
|
||||||
pub use theme::ActiveTheme;
|
pub use theme::ActiveTheme;
|
||||||
|
|
||||||
use strum::EnumIter;
|
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, EnumIter)]
|
|
||||||
pub enum IconSide {
|
|
||||||
#[default]
|
|
||||||
Left,
|
|
||||||
Right,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Copy, Clone, EnumIter, strum::Display)]
|
|
||||||
pub enum InteractionState {
|
|
||||||
/// An element that is enabled and not hovered, active, focused, or disabled.
|
|
||||||
///
|
|
||||||
/// This is often referred to as the "default" state.
|
|
||||||
#[default]
|
|
||||||
Enabled,
|
|
||||||
/// An element that is hovered.
|
|
||||||
Hovered,
|
|
||||||
/// An element has an active mouse down or touch start event on it.
|
|
||||||
Active,
|
|
||||||
/// An element that is focused using the keyboard.
|
|
||||||
Focused,
|
|
||||||
/// An element that is disabled.
|
|
||||||
Disabled,
|
|
||||||
/// A toggleable element that is selected, like the active button in a
|
|
||||||
/// button toggle group.
|
|
||||||
Selected,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl InteractionState {
|
|
||||||
pub fn if_enabled(&self, enabled: bool) -> Self {
|
|
||||||
if enabled {
|
|
||||||
*self
|
|
||||||
} else {
|
|
||||||
InteractionState::Disabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
|
||||||
pub enum Selection {
|
|
||||||
#[default]
|
|
||||||
Unselected,
|
|
||||||
Indeterminate,
|
|
||||||
Selected,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Selection {
|
|
||||||
pub fn inverse(&self) -> Self {
|
|
||||||
match self {
|
|
||||||
Self::Unselected | Self::Indeterminate => Self::Selected,
|
|
||||||
Self::Selected => Self::Unselected,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
26
crates/ui2/src/selectable.rs
Normal file
26
crates/ui2/src/selectable.rs
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
use gpui::{AnyView, WindowContext};
|
||||||
|
|
||||||
|
pub trait Selectable {
|
||||||
|
fn selected(self, selected: bool) -> Self;
|
||||||
|
fn selected_tooltip(
|
||||||
|
self,
|
||||||
|
tooltip: Box<dyn Fn(&mut WindowContext) -> AnyView + 'static>,
|
||||||
|
) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, PartialEq, Eq, Hash, Clone, Copy)]
|
||||||
|
pub enum Selection {
|
||||||
|
#[default]
|
||||||
|
Unselected,
|
||||||
|
Indeterminate,
|
||||||
|
Selected,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Selection {
|
||||||
|
pub fn inverse(&self) -> Self {
|
||||||
|
match self {
|
||||||
|
Self::Unselected | Self::Indeterminate => Self::Selected,
|
||||||
|
Self::Selected => Self::Unselected,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,7 +1,7 @@
|
||||||
use gpui::{Hsla, WindowContext};
|
use gpui::{Hsla, WindowContext};
|
||||||
use theme::ActiveTheme;
|
use theme::ActiveTheme;
|
||||||
|
|
||||||
#[derive(Default, PartialEq, Copy, Clone)]
|
#[derive(Debug, Default, PartialEq, Copy, Clone)]
|
||||||
pub enum Color {
|
pub enum Color {
|
||||||
#[default]
|
#[default]
|
||||||
Default,
|
Default,
|
||||||
|
|
|
@ -12,13 +12,19 @@
|
||||||
#![doc = include_str!("../docs/building-ui.md")]
|
#![doc = include_str!("../docs/building-ui.md")]
|
||||||
#![doc = include_str!("../docs/todo.md")]
|
#![doc = include_str!("../docs/todo.md")]
|
||||||
|
|
||||||
|
mod clickable;
|
||||||
mod components;
|
mod components;
|
||||||
|
mod fixed;
|
||||||
pub mod prelude;
|
pub mod prelude;
|
||||||
|
mod selectable;
|
||||||
mod styled_ext;
|
mod styled_ext;
|
||||||
mod styles;
|
mod styles;
|
||||||
pub mod utils;
|
pub mod utils;
|
||||||
|
|
||||||
|
pub use clickable::*;
|
||||||
pub use components::*;
|
pub use components::*;
|
||||||
|
pub use fixed::*;
|
||||||
pub use prelude::*;
|
pub use prelude::*;
|
||||||
|
pub use selectable::*;
|
||||||
pub use styled_ext::*;
|
pub use styled_ext::*;
|
||||||
pub use styles::*;
|
pub use styles::*;
|
||||||
|
|
37
crates/welcome2/Cargo.toml
Normal file
37
crates/welcome2/Cargo.toml
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
[package]
|
||||||
|
name = "welcome2"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
path = "src/welcome.rs"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
test-support = []
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
client = { package = "client2", path = "../client2" }
|
||||||
|
editor = { package = "editor2", path = "../editor2" }
|
||||||
|
fs = { package = "fs2", path = "../fs2" }
|
||||||
|
fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
|
||||||
|
gpui = { package = "gpui2", path = "../gpui2" }
|
||||||
|
ui = { package = "ui2", path = "../ui2" }
|
||||||
|
db = { package = "db2", path = "../db2" }
|
||||||
|
install_cli = { package = "install_cli2", path = "../install_cli2" }
|
||||||
|
project = { package = "project2", path = "../project2" }
|
||||||
|
settings = { package = "settings2", path = "../settings2" }
|
||||||
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
|
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||||
|
util = { path = "../util" }
|
||||||
|
picker = { package = "picker2", path = "../picker2" }
|
||||||
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
|
# vim = { package = "vim2", path = "../vim2" }
|
||||||
|
|
||||||
|
anyhow.workspace = true
|
||||||
|
log.workspace = true
|
||||||
|
schemars.workspace = true
|
||||||
|
serde.workspace = true
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
208
crates/welcome2/src/base_keymap_picker.rs
Normal file
208
crates/welcome2/src/base_keymap_picker.rs
Normal file
|
@ -0,0 +1,208 @@
|
||||||
|
use super::base_keymap_setting::BaseKeymap;
|
||||||
|
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||||
|
use gpui::{
|
||||||
|
actions, AppContext, DismissEvent, EventEmitter, FocusableView, ParentElement, Render, Task,
|
||||||
|
View, ViewContext, VisualContext, WeakView,
|
||||||
|
};
|
||||||
|
use picker::{Picker, PickerDelegate};
|
||||||
|
use project::Fs;
|
||||||
|
use settings::{update_settings_file, Settings};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use ui::ListItem;
|
||||||
|
use util::ResultExt;
|
||||||
|
use workspace::{ui::HighlightedLabel, Workspace};
|
||||||
|
|
||||||
|
actions!(ToggleBaseKeymapSelector);
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||||
|
workspace.register_action(toggle);
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn toggle(
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
_: &ToggleBaseKeymapSelector,
|
||||||
|
cx: &mut ViewContext<Workspace>,
|
||||||
|
) {
|
||||||
|
let fs = workspace.app_state().fs.clone();
|
||||||
|
workspace.toggle_modal(cx, |cx| {
|
||||||
|
BaseKeymapSelector::new(
|
||||||
|
BaseKeymapSelectorDelegate::new(cx.view().downgrade(), fs, cx),
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BaseKeymapSelector {
|
||||||
|
focus_handle: gpui::FocusHandle,
|
||||||
|
picker: View<Picker<BaseKeymapSelectorDelegate>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FocusableView for BaseKeymapSelector {
|
||||||
|
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<DismissEvent> for BaseKeymapSelector {}
|
||||||
|
|
||||||
|
impl BaseKeymapSelector {
|
||||||
|
pub fn new(
|
||||||
|
delegate: BaseKeymapSelectorDelegate,
|
||||||
|
cx: &mut ViewContext<BaseKeymapSelector>,
|
||||||
|
) -> Self {
|
||||||
|
let picker = cx.build_view(|cx| Picker::new(delegate, cx));
|
||||||
|
let focus_handle = cx.focus_handle();
|
||||||
|
Self {
|
||||||
|
focus_handle,
|
||||||
|
picker,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for BaseKeymapSelector {
|
||||||
|
type Element = View<Picker<BaseKeymapSelectorDelegate>>;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||||
|
self.picker.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct BaseKeymapSelectorDelegate {
|
||||||
|
view: WeakView<BaseKeymapSelector>,
|
||||||
|
matches: Vec<StringMatch>,
|
||||||
|
selected_index: usize,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseKeymapSelectorDelegate {
|
||||||
|
fn new(
|
||||||
|
weak_view: WeakView<BaseKeymapSelector>,
|
||||||
|
fs: Arc<dyn Fs>,
|
||||||
|
cx: &mut ViewContext<BaseKeymapSelector>,
|
||||||
|
) -> Self {
|
||||||
|
let base = BaseKeymap::get(None, cx);
|
||||||
|
let selected_index = BaseKeymap::OPTIONS
|
||||||
|
.iter()
|
||||||
|
.position(|(_, value)| value == base)
|
||||||
|
.unwrap_or(0);
|
||||||
|
Self {
|
||||||
|
view: weak_view,
|
||||||
|
matches: Vec::new(),
|
||||||
|
selected_index,
|
||||||
|
fs,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PickerDelegate for BaseKeymapSelectorDelegate {
|
||||||
|
type ListItem = ui::ListItem;
|
||||||
|
|
||||||
|
fn placeholder_text(&self) -> Arc<str> {
|
||||||
|
"Select a base keymap...".into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn match_count(&self) -> usize {
|
||||||
|
self.matches.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn selected_index(&self) -> usize {
|
||||||
|
self.selected_index
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_selected_index(
|
||||||
|
&mut self,
|
||||||
|
ix: usize,
|
||||||
|
_: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
|
||||||
|
) {
|
||||||
|
self.selected_index = ix;
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_matches(
|
||||||
|
&mut self,
|
||||||
|
query: String,
|
||||||
|
cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>,
|
||||||
|
) -> Task<()> {
|
||||||
|
let background = cx.background_executor().clone();
|
||||||
|
let candidates = BaseKeymap::names()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(id, name)| StringMatchCandidate {
|
||||||
|
id,
|
||||||
|
char_bag: name.into(),
|
||||||
|
string: name.into(),
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
cx.spawn(|this, mut cx| async move {
|
||||||
|
let matches = if query.is_empty() {
|
||||||
|
candidates
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
.map(|(index, candidate)| StringMatch {
|
||||||
|
candidate_id: index,
|
||||||
|
string: candidate.string,
|
||||||
|
positions: Vec::new(),
|
||||||
|
score: 0.0,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
match_strings(
|
||||||
|
&candidates,
|
||||||
|
&query,
|
||||||
|
false,
|
||||||
|
100,
|
||||||
|
&Default::default(),
|
||||||
|
background,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
this.update(&mut cx, |this, _| {
|
||||||
|
this.delegate.matches = matches;
|
||||||
|
this.delegate.selected_index = this
|
||||||
|
.delegate
|
||||||
|
.selected_index
|
||||||
|
.min(this.delegate.matches.len().saturating_sub(1));
|
||||||
|
})
|
||||||
|
.log_err();
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {
|
||||||
|
if let Some(selection) = self.matches.get(self.selected_index) {
|
||||||
|
let base_keymap = BaseKeymap::from_names(&selection.string);
|
||||||
|
update_settings_file::<BaseKeymap>(self.fs.clone(), cx, move |setting| {
|
||||||
|
*setting = Some(base_keymap)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
self.view
|
||||||
|
.update(cx, |_, cx| {
|
||||||
|
cx.emit(DismissEvent);
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<BaseKeymapSelectorDelegate>>) {}
|
||||||
|
|
||||||
|
fn render_match(
|
||||||
|
&self,
|
||||||
|
ix: usize,
|
||||||
|
selected: bool,
|
||||||
|
_cx: &mut gpui::ViewContext<Picker<Self>>,
|
||||||
|
) -> Option<Self::ListItem> {
|
||||||
|
let keymap_match = &self.matches[ix];
|
||||||
|
|
||||||
|
Some(
|
||||||
|
ListItem::new(ix)
|
||||||
|
.selected(selected)
|
||||||
|
.inset(true)
|
||||||
|
.child(HighlightedLabel::new(
|
||||||
|
keymap_match.string.clone(),
|
||||||
|
keymap_match.positions.clone(),
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
65
crates/welcome2/src/base_keymap_setting.rs
Normal file
65
crates/welcome2/src/base_keymap_setting.rs
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
use schemars::JsonSchema;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use settings::Settings;
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, Default)]
|
||||||
|
pub enum BaseKeymap {
|
||||||
|
#[default]
|
||||||
|
VSCode,
|
||||||
|
JetBrains,
|
||||||
|
SublimeText,
|
||||||
|
Atom,
|
||||||
|
TextMate,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BaseKeymap {
|
||||||
|
pub const OPTIONS: [(&'static str, Self); 5] = [
|
||||||
|
("VSCode (Default)", Self::VSCode),
|
||||||
|
("Atom", Self::Atom),
|
||||||
|
("JetBrains", Self::JetBrains),
|
||||||
|
("Sublime Text", Self::SublimeText),
|
||||||
|
("TextMate", Self::TextMate),
|
||||||
|
];
|
||||||
|
|
||||||
|
pub fn asset_path(&self) -> Option<&'static str> {
|
||||||
|
match self {
|
||||||
|
BaseKeymap::JetBrains => Some("keymaps/jetbrains.json"),
|
||||||
|
BaseKeymap::SublimeText => Some("keymaps/sublime_text.json"),
|
||||||
|
BaseKeymap::Atom => Some("keymaps/atom.json"),
|
||||||
|
BaseKeymap::TextMate => Some("keymaps/textmate.json"),
|
||||||
|
BaseKeymap::VSCode => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn names() -> impl Iterator<Item = &'static str> {
|
||||||
|
Self::OPTIONS.iter().map(|(name, _)| *name)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_names(option: &str) -> BaseKeymap {
|
||||||
|
Self::OPTIONS
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.find_map(|(name, value)| (name == option).then(|| value))
|
||||||
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Settings for BaseKeymap {
|
||||||
|
const KEY: Option<&'static str> = Some("base_keymap");
|
||||||
|
|
||||||
|
type FileContent = Option<Self>;
|
||||||
|
|
||||||
|
fn load(
|
||||||
|
default_value: &Self::FileContent,
|
||||||
|
user_values: &[&Self::FileContent],
|
||||||
|
_: &mut gpui::AppContext,
|
||||||
|
) -> anyhow::Result<Self>
|
||||||
|
where
|
||||||
|
Self: Sized,
|
||||||
|
{
|
||||||
|
Ok(user_values
|
||||||
|
.first()
|
||||||
|
.and_then(|v| **v)
|
||||||
|
.unwrap_or(default_value.unwrap()))
|
||||||
|
}
|
||||||
|
}
|
281
crates/welcome2/src/welcome.rs
Normal file
281
crates/welcome2/src/welcome.rs
Normal file
|
@ -0,0 +1,281 @@
|
||||||
|
mod base_keymap_picker;
|
||||||
|
mod base_keymap_setting;
|
||||||
|
|
||||||
|
use db::kvp::KEY_VALUE_STORE;
|
||||||
|
use gpui::{
|
||||||
|
div, red, AnyElement, AppContext, Div, Element, EventEmitter, FocusHandle, Focusable,
|
||||||
|
FocusableView, InteractiveElement, ParentElement, Render, Styled, Subscription, View,
|
||||||
|
ViewContext, VisualContext, WeakView, WindowContext,
|
||||||
|
};
|
||||||
|
use settings::{Settings, SettingsStore};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use workspace::{
|
||||||
|
dock::DockPosition,
|
||||||
|
item::{Item, ItemEvent},
|
||||||
|
open_new, AppState, Welcome, Workspace, WorkspaceId,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub use base_keymap_setting::BaseKeymap;
|
||||||
|
|
||||||
|
pub const FIRST_OPEN: &str = "first_open";
|
||||||
|
|
||||||
|
pub fn init(cx: &mut AppContext) {
|
||||||
|
BaseKeymap::register(cx);
|
||||||
|
|
||||||
|
cx.observe_new_views(|workspace: &mut Workspace, _cx| {
|
||||||
|
workspace.register_action(|workspace, _: &Welcome, cx| {
|
||||||
|
let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
|
||||||
|
workspace.add_item(Box::new(welcome_page), cx)
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
base_keymap_picker::init(cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn show_welcome_experience(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
open_new(&app_state, cx, |workspace, cx| {
|
||||||
|
workspace.toggle_dock(DockPosition::Left, cx);
|
||||||
|
let welcome_page = cx.build_view(|cx| WelcomePage::new(workspace, cx));
|
||||||
|
workspace.add_item_to_center(Box::new(welcome_page.clone()), cx);
|
||||||
|
cx.focus_view(&welcome_page);
|
||||||
|
cx.notify();
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
|
||||||
|
db::write_and_log(cx, || {
|
||||||
|
KEY_VALUE_STORE.write_kvp(FIRST_OPEN.to_string(), "false".to_string())
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WelcomePage {
|
||||||
|
workspace: WeakView<Workspace>,
|
||||||
|
focus_handle: FocusHandle,
|
||||||
|
_settings_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for WelcomePage {
|
||||||
|
type Element = Focusable<Div>;
|
||||||
|
|
||||||
|
fn render(&mut self, _cx: &mut gpui::ViewContext<Self>) -> Self::Element {
|
||||||
|
// todo!(welcome_ui)
|
||||||
|
// let self_handle = cx.handle();
|
||||||
|
// let theme = cx.theme();
|
||||||
|
// let width = theme.welcome.page_width;
|
||||||
|
|
||||||
|
// let telemetry_settings = TelemetrySettings::get(None, cx);
|
||||||
|
// let vim_mode_setting = VimModeSettings::get(cx);
|
||||||
|
|
||||||
|
div()
|
||||||
|
.track_focus(&self.focus_handle)
|
||||||
|
.child(div().size_full().bg(red()).child("Welcome!"))
|
||||||
|
//todo!()
|
||||||
|
// PaneBackdrop::new(
|
||||||
|
// self_handle.id(),
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// theme::ui::svg(&theme.welcome.logo)
|
||||||
|
// .aligned()
|
||||||
|
// .contained()
|
||||||
|
// .aligned(),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// "Code at the speed of thought",
|
||||||
|
// theme.welcome.logo_subheading.text.clone(),
|
||||||
|
// )
|
||||||
|
// .aligned()
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.logo_subheading.container),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.heading_group)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(width),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(theme::ui::cta_button::<theme_selector::Toggle, _, _, _>(
|
||||||
|
// "Choose a theme",
|
||||||
|
// width,
|
||||||
|
// &theme.welcome.button,
|
||||||
|
// cx,
|
||||||
|
// |_, this, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// workspace.update(cx, |workspace, cx| {
|
||||||
|
// theme_selector::toggle(workspace, &Default::default(), cx)
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
// .with_child(theme::ui::cta_button::<ToggleBaseKeymapSelector, _, _, _>(
|
||||||
|
// "Choose a keymap",
|
||||||
|
// width,
|
||||||
|
// &theme.welcome.button,
|
||||||
|
// cx,
|
||||||
|
// |_, this, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// workspace.update(cx, |workspace, cx| {
|
||||||
|
// base_keymap_picker::toggle(
|
||||||
|
// workspace,
|
||||||
|
// &Default::default(),
|
||||||
|
// cx,
|
||||||
|
// )
|
||||||
|
// })
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
// .with_child(theme::ui::cta_button::<install_cli::Install, _, _, _>(
|
||||||
|
// "Install the CLI",
|
||||||
|
// width,
|
||||||
|
// &theme.welcome.button,
|
||||||
|
// cx,
|
||||||
|
// |_, _, cx| {
|
||||||
|
// cx.app_context()
|
||||||
|
// .spawn(|cx| async move { install_cli::install_cli(&cx).await })
|
||||||
|
// .detach_and_log_err(cx);
|
||||||
|
// },
|
||||||
|
// ))
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.button_group)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(width),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// theme::ui::checkbox::<Diagnostics, Self, _>(
|
||||||
|
// "Enable vim mode",
|
||||||
|
// &theme.welcome.checkbox,
|
||||||
|
// vim_mode_setting,
|
||||||
|
// 0,
|
||||||
|
// cx,
|
||||||
|
// |this, checked, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
|
// update_settings_file::<VimModeSetting>(
|
||||||
|
// fs,
|
||||||
|
// cx,
|
||||||
|
// move |setting| *setting = Some(checked),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.checkbox_container),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// theme::ui::checkbox_with_label::<Metrics, _, Self, _>(
|
||||||
|
// Flex::column()
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// "Send anonymous usage data",
|
||||||
|
// theme.welcome.checkbox.label.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.checkbox.label.container),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// Label::new(
|
||||||
|
// "Help > View Telemetry",
|
||||||
|
// theme.welcome.usage_note.text.clone(),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.usage_note.container),
|
||||||
|
// ),
|
||||||
|
// &theme.welcome.checkbox,
|
||||||
|
// telemetry_settings.metrics,
|
||||||
|
// 0,
|
||||||
|
// cx,
|
||||||
|
// |this, checked, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
|
// update_settings_file::<TelemetrySettings>(
|
||||||
|
// fs,
|
||||||
|
// cx,
|
||||||
|
// move |setting| setting.metrics = Some(checked),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.checkbox_container),
|
||||||
|
// )
|
||||||
|
// .with_child(
|
||||||
|
// theme::ui::checkbox::<Diagnostics, Self, _>(
|
||||||
|
// "Send crash reports",
|
||||||
|
// &theme.welcome.checkbox,
|
||||||
|
// telemetry_settings.diagnostics,
|
||||||
|
// 1,
|
||||||
|
// cx,
|
||||||
|
// |this, checked, cx| {
|
||||||
|
// if let Some(workspace) = this.workspace.upgrade(cx) {
|
||||||
|
// let fs = workspace.read(cx).app_state().fs.clone();
|
||||||
|
// update_settings_file::<TelemetrySettings>(
|
||||||
|
// fs,
|
||||||
|
// cx,
|
||||||
|
// move |setting| setting.diagnostics = Some(checked),
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// },
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.checkbox_container),
|
||||||
|
// )
|
||||||
|
// .contained()
|
||||||
|
// .with_style(theme.welcome.checkbox_group)
|
||||||
|
// .constrained()
|
||||||
|
// .with_width(width),
|
||||||
|
// )
|
||||||
|
// .constrained()
|
||||||
|
// .with_max_width(width)
|
||||||
|
// .contained()
|
||||||
|
// .with_uniform_padding(10.)
|
||||||
|
// .aligned()
|
||||||
|
// .into_any(),
|
||||||
|
// )
|
||||||
|
// .into_any_named("welcome page")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WelcomePage {
|
||||||
|
pub fn new(workspace: &Workspace, cx: &mut ViewContext<Self>) -> Self {
|
||||||
|
WelcomePage {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
workspace: workspace.weak_handle(),
|
||||||
|
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ItemEvent> for WelcomePage {}
|
||||||
|
|
||||||
|
impl FocusableView for WelcomePage {
|
||||||
|
fn focus_handle(&self, _: &AppContext) -> gpui::FocusHandle {
|
||||||
|
self.focus_handle.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for WelcomePage {
|
||||||
|
fn tab_content(&self, _: Option<usize>, _: &WindowContext) -> AnyElement {
|
||||||
|
"Welcome to Zed!".into_any()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn show_toolbar(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn clone_on_split(
|
||||||
|
&self,
|
||||||
|
_workspace_id: WorkspaceId,
|
||||||
|
cx: &mut ViewContext<Self>,
|
||||||
|
) -> Option<View<Self>> {
|
||||||
|
Some(cx.build_view(|cx| WelcomePage {
|
||||||
|
focus_handle: cx.focus_handle(),
|
||||||
|
workspace: self.workspace.clone(),
|
||||||
|
_settings_subscription: cx.observe_global::<SettingsStore>(move |_, cx| cx.notify()),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,8 +7,8 @@ use gpui::{
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
|
use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
|
||||||
|
|
||||||
pub enum PanelEvent {
|
pub enum PanelEvent {
|
||||||
ChangePosition,
|
ChangePosition,
|
||||||
|
@ -686,22 +686,26 @@ impl Render for PanelButtons {
|
||||||
let name = entry.panel.persistent_name();
|
let name = entry.panel.persistent_name();
|
||||||
let panel = entry.panel.clone();
|
let panel = entry.panel.clone();
|
||||||
|
|
||||||
let mut button: IconButton = if i == active_index && is_open {
|
let is_active_button = i == active_index && is_open;
|
||||||
|
|
||||||
|
let (action, tooltip) = if is_active_button {
|
||||||
let action = dock.toggle_action();
|
let action = dock.toggle_action();
|
||||||
|
|
||||||
let tooltip: SharedString =
|
let tooltip: SharedString =
|
||||||
format!("Close {} dock", dock.position.to_label()).into();
|
format!("Close {} dock", dock.position.to_label()).into();
|
||||||
IconButton::new(name, icon)
|
|
||||||
.state(InteractionState::Active)
|
(action, tooltip)
|
||||||
.action(action.boxed_clone())
|
|
||||||
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
|
||||||
} else {
|
} else {
|
||||||
let action = entry.panel.toggle_action(cx);
|
let action = entry.panel.toggle_action(cx);
|
||||||
|
|
||||||
IconButton::new(name, icon)
|
(action, name.into())
|
||||||
.action(action.boxed_clone())
|
|
||||||
.tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let button = IconButton::new(name, icon)
|
||||||
|
.selected(is_active_button)
|
||||||
|
.action(action.boxed_clone())
|
||||||
|
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx));
|
||||||
|
|
||||||
Some(
|
Some(
|
||||||
menu_handle(name)
|
menu_handle(name)
|
||||||
.menu(move |cx| {
|
.menu(move |cx| {
|
||||||
|
|
|
@ -1482,18 +1482,14 @@ impl Pane {
|
||||||
.gap_px()
|
.gap_px()
|
||||||
.child(
|
.child(
|
||||||
div().border().border_color(gpui::red()).child(
|
div().border().border_color(gpui::red()).child(
|
||||||
IconButton::new("navigate_backward", Icon::ArrowLeft).state(
|
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||||
InteractionState::Enabled
|
.disabled(!self.can_navigate_backward()),
|
||||||
.if_enabled(self.can_navigate_backward()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.child(
|
.child(
|
||||||
div().border().border_color(gpui::red()).child(
|
div().border().border_color(gpui::red()).child(
|
||||||
IconButton::new("navigate_forward", Icon::ArrowRight).state(
|
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||||
InteractionState::Enabled
|
.disabled(!self.can_navigate_forward()),
|
||||||
.if_enabled(self.can_navigate_forward()),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
|
@ -5,7 +5,7 @@ use gpui::{
|
||||||
div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
|
div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
|
||||||
WindowContext,
|
WindowContext,
|
||||||
};
|
};
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, Button, Icon, IconButton};
|
use ui::{h_stack, Button, Icon, IconButton};
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ use gpui::{
|
||||||
div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
|
div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
|
||||||
ViewContext, WindowContext,
|
ViewContext, WindowContext,
|
||||||
};
|
};
|
||||||
use theme2::ActiveTheme;
|
use ui::prelude::*;
|
||||||
use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
|
use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
|
||||||
|
|
||||||
pub enum ToolbarItemEvent {
|
pub enum ToolbarItemEvent {
|
||||||
|
|
|
@ -1808,22 +1808,22 @@ impl Workspace {
|
||||||
pane
|
pane
|
||||||
}
|
}
|
||||||
|
|
||||||
// pub fn add_item_to_center(
|
pub fn add_item_to_center(
|
||||||
// &mut self,
|
&mut self,
|
||||||
// item: Box<dyn ItemHandle>,
|
item: Box<dyn ItemHandle>,
|
||||||
// cx: &mut ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
// ) -> bool {
|
) -> bool {
|
||||||
// if let Some(center_pane) = self.last_active_center_pane.clone() {
|
if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||||
// if let Some(center_pane) = center_pane.upgrade(cx) {
|
if let Some(center_pane) = center_pane.upgrade() {
|
||||||
// center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
||||||
// true
|
true
|
||||||
// } else {
|
} else {
|
||||||
// false
|
false
|
||||||
// }
|
}
|
||||||
// } else {
|
} else {
|
||||||
// false
|
false
|
||||||
// }
|
}
|
||||||
// }
|
}
|
||||||
|
|
||||||
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||||
self.active_pane
|
self.active_pane
|
||||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
||||||
description = "The fast, collaborative code editor."
|
description = "The fast, collaborative code editor."
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
name = "zed"
|
name = "zed"
|
||||||
version = "0.115.0"
|
version = "0.116.0"
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
|
|
|
@ -66,12 +66,12 @@ shellexpand = "2.1.0"
|
||||||
text = { package = "text2", path = "../text2" }
|
text = { package = "text2", path = "../text2" }
|
||||||
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
||||||
theme = { package = "theme2", path = "../theme2" }
|
theme = { package = "theme2", path = "../theme2" }
|
||||||
# theme_selector = { path = "../theme_selector" }
|
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||||
util = { path = "../util" }
|
util = { path = "../util" }
|
||||||
# semantic_index = { path = "../semantic_index" }
|
# semantic_index = { path = "../semantic_index" }
|
||||||
# vim = { path = "../vim" }
|
# vim = { path = "../vim" }
|
||||||
workspace = { package = "workspace2", path = "../workspace2" }
|
workspace = { package = "workspace2", path = "../workspace2" }
|
||||||
# welcome = { path = "../welcome" }
|
welcome = { package = "welcome2", path = "../welcome2" }
|
||||||
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
|
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||||
|
|
|
@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE;
|
||||||
use editor::Editor;
|
use editor::Editor;
|
||||||
use fs::RealFs;
|
use fs::RealFs;
|
||||||
use futures::StreamExt;
|
use futures::StreamExt;
|
||||||
use gpui::{Action, App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
|
use gpui::{App, AppContext, AsyncAppContext, Context, SemanticVersion, Task};
|
||||||
use isahc::{prelude::Configurable, Request};
|
use isahc::{prelude::Configurable, Request};
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use log::LevelFilter;
|
use log::LevelFilter;
|
||||||
|
@ -36,7 +36,7 @@ use std::{
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{
|
sync::{
|
||||||
atomic::{AtomicU32, Ordering},
|
atomic::{AtomicU32, Ordering},
|
||||||
Arc,
|
Arc, Weak,
|
||||||
},
|
},
|
||||||
thread,
|
thread,
|
||||||
};
|
};
|
||||||
|
@ -48,6 +48,7 @@ use util::{
|
||||||
paths, ResultExt,
|
paths, ResultExt,
|
||||||
};
|
};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||||
use workspace::{AppState, WorkspaceStore};
|
use workspace::{AppState, WorkspaceStore};
|
||||||
use zed2::{
|
use zed2::{
|
||||||
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
|
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
|
||||||
|
@ -103,16 +104,15 @@ fn main() {
|
||||||
let listener = Arc::new(listener);
|
let listener = Arc::new(listener);
|
||||||
let open_listener = listener.clone();
|
let open_listener = listener.clone();
|
||||||
app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
|
app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
|
||||||
app.on_reopen(move |_cx| {
|
app.on_reopen(move |cx| {
|
||||||
// todo!("workspace")
|
if cx.has_global::<Weak<AppState>>() {
|
||||||
// if cx.has_global::<Weak<AppState>>() {
|
if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
|
||||||
// if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
|
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||||
// workspace::open_new(&app_state, cx, |workspace, cx| {
|
Editor::new_file(workspace, &Default::default(), cx)
|
||||||
// Editor::new_file(workspace, &Default::default(), cx)
|
})
|
||||||
// })
|
.detach();
|
||||||
// .detach();
|
}
|
||||||
// }
|
}
|
||||||
// }
|
|
||||||
});
|
});
|
||||||
|
|
||||||
app.run(move |cx| {
|
app.run(move |cx| {
|
||||||
|
@ -164,17 +164,16 @@ fn main() {
|
||||||
// assistant::init(cx);
|
// assistant::init(cx);
|
||||||
// component_test::init(cx);
|
// component_test::init(cx);
|
||||||
|
|
||||||
// cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
|
|
||||||
// cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
// cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||||
// .detach();
|
// .detach();
|
||||||
// watch_file_types(fs.clone(), cx);
|
watch_file_types(fs.clone(), cx);
|
||||||
|
|
||||||
languages.set_theme(cx.theme().clone());
|
languages.set_theme(cx.theme().clone());
|
||||||
// cx.observe_global::<SettingsStore, _>({
|
cx.observe_global::<SettingsStore>({
|
||||||
// let languages = languages.clone();
|
let languages = languages.clone();
|
||||||
// move |cx| languages.set_theme(theme::current(cx).clone())
|
move |cx| languages.set_theme(cx.theme().clone())
|
||||||
// })
|
})
|
||||||
// .detach();
|
.detach();
|
||||||
|
|
||||||
client.telemetry().start(installation_id, session_id, cx);
|
client.telemetry().start(installation_id, session_id, cx);
|
||||||
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
|
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
|
||||||
|
@ -193,7 +192,6 @@ fn main() {
|
||||||
fs,
|
fs,
|
||||||
build_window_options,
|
build_window_options,
|
||||||
call_factory: call::Call::new,
|
call_factory: call::Call::new,
|
||||||
// background_actions: todo!("ask Mikayla"),
|
|
||||||
workspace_store,
|
workspace_store,
|
||||||
node_runtime,
|
node_runtime,
|
||||||
});
|
});
|
||||||
|
@ -219,14 +217,13 @@ fn main() {
|
||||||
|
|
||||||
// journal2::init(app_state.clone(), cx);
|
// journal2::init(app_state.clone(), cx);
|
||||||
// language_selector::init(cx);
|
// language_selector::init(cx);
|
||||||
// theme_selector::init(cx);
|
theme_selector::init(cx);
|
||||||
// activity_indicator::init(cx);
|
// activity_indicator::init(cx);
|
||||||
// language_tools::init(cx);
|
// language_tools::init(cx);
|
||||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||||
collab_ui::init(&app_state, cx);
|
collab_ui::init(&app_state, cx);
|
||||||
// feedback::init(cx);
|
// feedback::init(cx);
|
||||||
// welcome::init(cx);
|
welcome::init(cx);
|
||||||
// zed::init(&app_state, cx);
|
|
||||||
|
|
||||||
// cx.set_menus(menus::menus());
|
// cx.set_menus(menus::menus());
|
||||||
initialize_workspace(app_state.clone(), cx);
|
initialize_workspace(app_state.clone(), cx);
|
||||||
|
@ -279,17 +276,18 @@ fn main() {
|
||||||
.detach();
|
.detach();
|
||||||
}
|
}
|
||||||
Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
|
Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
|
||||||
todo!()
|
triggered_authentication = true;
|
||||||
// triggered_authentication = true;
|
let app_state = app_state.clone();
|
||||||
// let app_state = app_state.clone();
|
let client = client.clone();
|
||||||
// let client = client.clone();
|
cx.spawn(|mut cx| async move {
|
||||||
// cx.spawn(|mut cx| async move {
|
// ignore errors here, we'll show a generic "not signed in"
|
||||||
// // ignore errors here, we'll show a generic "not signed in"
|
let _ = authenticate(client, &cx).await;
|
||||||
// let _ = authenticate(client, &cx).await;
|
//todo!()
|
||||||
// cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
|
// cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
|
||||||
// .await
|
// .await
|
||||||
// })
|
anyhow::Ok(())
|
||||||
// .detach_and_log_err(cx)
|
})
|
||||||
|
.detach_and_log_err(cx)
|
||||||
}
|
}
|
||||||
Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
|
Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
|
||||||
todo!()
|
todo!()
|
||||||
|
@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
|
||||||
if client::IMPERSONATE_LOGIN.is_some() {
|
if client::IMPERSONATE_LOGIN.is_some() {
|
||||||
client.authenticate_and_connect(false, &cx).await?;
|
client.authenticate_and_connect(false, &cx).await?;
|
||||||
}
|
}
|
||||||
} else if client.has_keychain_credentials(&cx).await {
|
} else if client.has_keychain_credentials(&cx) {
|
||||||
client.authenticate_and_connect(true, &cx).await?;
|
client.authenticate_and_connect(true, &cx).await?;
|
||||||
}
|
}
|
||||||
Ok::<_, anyhow::Error>(())
|
Ok::<_, anyhow::Error>(())
|
||||||
|
@ -368,10 +366,9 @@ async fn restore_or_create_workspace(app_state: &Arc<AppState>, mut cx: AsyncApp
|
||||||
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
|
cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx))?
|
||||||
.await
|
.await
|
||||||
.log_err();
|
.log_err();
|
||||||
// todo!(welcome)
|
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||||
//} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
cx.update(|cx| show_welcome_experience(app_state, cx))
|
||||||
//todo!()
|
.log_err();
|
||||||
// cx.update(|cx| show_welcome_experience(app_state, cx));
|
|
||||||
} else {
|
} else {
|
||||||
cx.update(|cx| {
|
cx.update(|cx| {
|
||||||
workspace::open_new(app_state, cx, |workspace, cx| {
|
workspace::open_new(app_state, cx, |workspace, cx| {
|
||||||
|
@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
// #[cfg(debug_assertions)]
|
#[cfg(debug_assertions)]
|
||||||
// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
|
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
|
||||||
// let mut events = fs
|
use std::time::Duration;
|
||||||
// .watch("styles/src".as_ref(), Duration::from_millis(100))
|
|
||||||
// .await;
|
|
||||||
// while (events.next().await).is_some() {
|
|
||||||
// let output = Command::new("npm")
|
|
||||||
// .current_dir("styles")
|
|
||||||
// .args(["run", "build"])
|
|
||||||
// .output()
|
|
||||||
// .await
|
|
||||||
// .log_err()?;
|
|
||||||
// if output.status.success() {
|
|
||||||
// cx.update(|cx| theme_selector::reload(cx))
|
|
||||||
// } else {
|
|
||||||
// eprintln!(
|
|
||||||
// "build script failed {}",
|
|
||||||
// String::from_utf8_lossy(&output.stderr)
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// Some(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(debug_assertions)]
|
let mut events = fs
|
||||||
// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
|
.watch(
|
||||||
// let mut events = fs
|
"crates/zed2/src/languages".as_ref(),
|
||||||
// .watch(
|
Duration::from_millis(100),
|
||||||
// "crates/zed/src/languages".as_ref(),
|
)
|
||||||
// Duration::from_millis(100),
|
.await;
|
||||||
// )
|
while (events.next().await).is_some() {
|
||||||
// .await;
|
languages.reload();
|
||||||
// while (events.next().await).is_some() {
|
}
|
||||||
// languages.reload();
|
Some(())
|
||||||
// }
|
|
||||||
// Some(())
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(debug_assertions)]
|
|
||||||
// fn watch_file_types(fs: Arc<dyn Fs>, cx: &mut AppContext) {
|
|
||||||
// cx.spawn(|mut cx| async move {
|
|
||||||
// let mut events = fs
|
|
||||||
// .watch(
|
|
||||||
// "assets/icons/file_icons/file_types.json".as_ref(),
|
|
||||||
// Duration::from_millis(100),
|
|
||||||
// )
|
|
||||||
// .await;
|
|
||||||
// while (events.next().await).is_some() {
|
|
||||||
// cx.update(|cx| {
|
|
||||||
// cx.update_global(|file_types, _| {
|
|
||||||
// *file_types = project_panel::file_associations::FileAssociations::new(Assets);
|
|
||||||
// });
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// .detach()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(not(debug_assertions))]
|
|
||||||
// async fn watch_themes(_fs: Arc<dyn Fs>, _cx: AsyncAppContext) -> Option<()> {
|
|
||||||
// None
|
|
||||||
// }
|
|
||||||
|
|
||||||
// #[cfg(not(debug_assertions))]
|
|
||||||
// async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
|
|
||||||
// None
|
|
||||||
//
|
|
||||||
|
|
||||||
// #[cfg(not(debug_assertions))]
|
|
||||||
// fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
|
||||||
|
|
||||||
pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
|
|
||||||
// &[
|
|
||||||
// ("Go to file", &file_finder::Toggle),
|
|
||||||
// ("Open command palette", &command_palette::Toggle),
|
|
||||||
// ("Open recent projects", &recent_projects::OpenRecent),
|
|
||||||
// ("Change your settings", &zed_actions::OpenSettings),
|
|
||||||
// ]
|
|
||||||
// todo!()
|
|
||||||
&[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
fn watch_file_types(fs: Arc<dyn fs::Fs>, cx: &mut AppContext) {
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
cx.spawn(|mut cx| async move {
|
||||||
|
let mut events = fs
|
||||||
|
.watch(
|
||||||
|
"assets/icons/file_icons/file_types.json".as_ref(),
|
||||||
|
Duration::from_millis(100),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
while (events.next().await).is_some() {
|
||||||
|
cx.update(|cx| {
|
||||||
|
cx.update_global(|file_types, _| {
|
||||||
|
*file_types = project_panel::file_associations::FileAssociations::new(Assets);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.ok();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.detach()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
async fn watch_languages(_: Arc<dyn Fs>, _: Arc<LanguageRegistry>) -> Option<()> {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(debug_assertions))]
|
||||||
|
fn watch_file_types(_fs: Arc<dyn Fs>, _cx: &mut AppContext) {}
|
||||||
|
|
|
@ -11,7 +11,7 @@ graph_file=target/crate-graph.html
|
||||||
cargo depgraph \
|
cargo depgraph \
|
||||||
--workspace-only \
|
--workspace-only \
|
||||||
--offline \
|
--offline \
|
||||||
--root=zed,cli,collab \
|
--root=zed2,cli,collab2 \
|
||||||
--dedup-transitive-deps \
|
--dedup-transitive-deps \
|
||||||
| dot -Tsvg > $graph_file
|
| dot -Tsvg > $graph_file
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue