mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 03:25:59 +00:00
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",
|
||||
]
|
||||
|
||||
[[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]]
|
||||
name = "thiserror"
|
||||
version = "1.0.48"
|
||||
|
@ -11054,6 +11075,31 @@ dependencies = [
|
|||
"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]]
|
||||
name = "which"
|
||||
version = "4.4.2"
|
||||
|
@ -11508,7 +11554,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.115.0"
|
||||
version = "0.116.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"ai",
|
||||
|
@ -11720,6 +11766,7 @@ dependencies = [
|
|||
"terminal_view2",
|
||||
"text2",
|
||||
"theme2",
|
||||
"theme_selector2",
|
||||
"thiserror",
|
||||
"tiny_http",
|
||||
"toml 0.5.11",
|
||||
|
@ -11757,6 +11804,7 @@ dependencies = [
|
|||
"urlencoding",
|
||||
"util",
|
||||
"uuid 1.4.1",
|
||||
"welcome2",
|
||||
"workspace2",
|
||||
"zed_actions2",
|
||||
]
|
||||
|
|
|
@ -107,6 +107,7 @@ members = [
|
|||
"crates/theme2",
|
||||
"crates/theme_importer",
|
||||
"crates/theme_selector",
|
||||
"crates/theme_selector2",
|
||||
"crates/ui2",
|
||||
"crates/util",
|
||||
"crates/semantic_index",
|
||||
|
@ -115,6 +116,7 @@ members = [
|
|||
"crates/vcs_menu",
|
||||
"crates/workspace2",
|
||||
"crates/welcome",
|
||||
"crates/welcome2",
|
||||
"crates/xtask",
|
||||
"crates/zed",
|
||||
"crates/zed2",
|
||||
|
|
|
@ -14,8 +14,8 @@ use client::{
|
|||
use collections::HashSet;
|
||||
use futures::{channel::oneshot, future::Shared, Future, FutureExt};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task,
|
||||
View, ViewContext, VisualContext, WeakModel, WeakView,
|
||||
AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, PromptLevel,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakModel, WeakView, WindowHandle,
|
||||
};
|
||||
pub use participant::ParticipantLocation;
|
||||
use postage::watch;
|
||||
|
@ -334,12 +334,55 @@ impl ActiveCall {
|
|||
pub fn join_channel(
|
||||
&mut self,
|
||||
channel_id: u64,
|
||||
requesting_window: Option<WindowHandle<Workspace>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Option<Model<Room>>>> {
|
||||
if let Some(room) = self.room().cloned() {
|
||||
if room.read(cx).channel_id() == Some(channel_id) {
|
||||
return Task::ready(Ok(Some(room)));
|
||||
} else {
|
||||
return cx.spawn(|_, _| async move {
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -693,8 +693,8 @@ impl Client {
|
|||
}
|
||||
}
|
||||
|
||||
pub async fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).await.is_some()
|
||||
pub fn has_keychain_credentials(&self, cx: &AsyncAppContext) -> bool {
|
||||
read_credentials_from_keychain(cx).is_some()
|
||||
}
|
||||
|
||||
#[async_recursion(?Send)]
|
||||
|
@ -725,7 +725,7 @@ impl Client {
|
|||
let mut read_from_keychain = false;
|
||||
let mut credentials = self.state.read().credentials.clone();
|
||||
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();
|
||||
}
|
||||
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() {
|
||||
return None;
|
||||
}
|
||||
|
|
|
@ -364,7 +364,8 @@ async fn test_joining_channel_ancestor_member(
|
|||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
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
|
||||
.is_ok());
|
||||
}
|
||||
|
@ -394,7 +395,9 @@ async fn test_channel_room(
|
|||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
@ -442,7 +445,9 @@ async fn test_channel_room(
|
|||
});
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
@ -559,12 +564,16 @@ async fn test_channel_room(
|
|||
});
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
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
|
||||
.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);
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
@ -627,7 +638,7 @@ async fn test_channel_jumping(executor: BackgroundExecutor, cx_a: &mut TestAppCo
|
|||
|
||||
active_call_a
|
||||
.update(cx_a, |active_call, cx| {
|
||||
active_call.join_channel(rust_id, cx)
|
||||
active_call.join_channel(rust_id, None, cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
@ -793,7 +804,7 @@ async fn test_call_from_channel(
|
|||
let active_call_b = cx_b.read(ActiveCall::global);
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
@ -1286,7 +1297,7 @@ async fn test_guest_access(
|
|||
|
||||
// Non-members should not be allowed to join
|
||||
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
|
||||
.is_err());
|
||||
|
||||
|
@ -1308,7 +1319,7 @@ async fn test_guest_access(
|
|||
|
||||
// Client B joins channel A as a guest
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
@ -1341,7 +1352,7 @@ async fn test_guest_access(
|
|||
assert_channels_list_shape(client_b.channel_store(), cx_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
|
||||
.unwrap();
|
||||
|
||||
|
@ -1372,7 +1383,7 @@ async fn test_invite_access(
|
|||
|
||||
// should not be allowed to join
|
||||
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
|
||||
.is_err());
|
||||
|
||||
|
@ -1390,7 +1401,7 @@ async fn test_invite_access(
|
|||
.unwrap();
|
||||
|
||||
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
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -510,9 +510,10 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
|||
|
||||
// Simultaneously join channel 1 and then channel 2
|
||||
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();
|
||||
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();
|
||||
|
||||
|
@ -538,7 +539,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
|||
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();
|
||||
c_invite.await.unwrap();
|
||||
|
@ -567,7 +569,8 @@ async fn test_joining_channels_and_calling_multiple_users_simultaneously(
|
|||
.unwrap();
|
||||
|
||||
// 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| {
|
||||
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::RequestAccepted => None,
|
||||
};
|
||||
dbg!(icon_path);
|
||||
Some(
|
||||
div()
|
||||
.flex_1()
|
||||
|
|
|
@ -37,7 +37,10 @@ use gpui::{
|
|||
};
|
||||
use project::Project;
|
||||
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 workspace::{notifications::NotifyResultExt, Workspace};
|
||||
|
||||
|
@ -298,6 +301,27 @@ impl Render for CollabTitlebarItem {
|
|||
})
|
||||
.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 fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
|
@ -5,10 +10,7 @@ use gpui::{
|
|||
Keystroke, ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::{
|
||||
cmp::{self, Reverse},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use ui::{h_stack, v_stack, HighlightedLabel, KeyBinding, ListItem};
|
||||
use util::{
|
||||
channel::{parse_zed_link, ReleaseChannel, RELEASE_CHANNEL},
|
||||
|
|
|
@ -30,11 +30,11 @@ pub trait FeatureFlagViewExt<V: 'static> {
|
|||
|
||||
impl<V> FeatureFlagViewExt<V> for ViewContext<'_, V>
|
||||
where
|
||||
V: 'static + Send + Sync,
|
||||
V: 'static,
|
||||
{
|
||||
fn observe_flag<T: FeatureFlag, F>(&mut self, callback: F) -> Subscription
|
||||
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| {
|
||||
let feature_flags = cx.global::<FeatureFlags>();
|
||||
|
|
|
@ -162,6 +162,7 @@ macro_rules! actions {
|
|||
|
||||
( $name:ident ) => {
|
||||
#[derive(::std::cmp::PartialEq, ::std::clone::Clone, ::std::default::Default, gpui::serde_derive::Deserialize, gpui::Action)]
|
||||
#[serde(crate = "gpui::serde")]
|
||||
pub struct $name;
|
||||
};
|
||||
|
||||
|
|
|
@ -111,7 +111,7 @@ pub struct Component<C> {
|
|||
|
||||
pub struct CompositeElementState<C: RenderOnce> {
|
||||
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> {
|
||||
|
@ -131,20 +131,40 @@ impl<C: RenderOnce> Element for Component<C> {
|
|||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
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);
|
||||
let state = CompositeElementState {
|
||||
rendered_element: Some(element),
|
||||
rendered_element_state: state,
|
||||
};
|
||||
(layout_id, state)
|
||||
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 {
|
||||
rendered_element: Some(element),
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint(self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
state
|
||||
.rendered_element
|
||||
.take()
|
||||
.unwrap()
|
||||
.paint(bounds, &mut state.rendered_element_state, cx);
|
||||
let element = state.rendered_element.take().unwrap();
|
||||
if let Some(element_id) = element.element_id() {
|
||||
cx.with_element_state(element_id, |element_state, cx| {
|
||||
let mut element_state = element_state.unwrap();
|
||||
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 content_size = Size {
|
||||
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
|
||||
|
@ -221,9 +221,7 @@ impl Element for UniformList {
|
|||
|
||||
let items = (self.render_items)(visible_range.clone(), cx);
|
||||
cx.with_z_index(1, |cx| {
|
||||
let content_mask = ContentMask {
|
||||
bounds: padded_bounds,
|
||||
};
|
||||
let content_mask = ContentMask { bounds };
|
||||
cx.with_content_mask(Some(content_mask), |cx| {
|
||||
for (item, ix) in items.into_iter().zip(visible_range) {
|
||||
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.
|
||||
fn content_mask(&self) -> ContentMask<Pixels> {
|
||||
self.window()
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, uniform_list, AppContext, Div, FocusHandle, FocusableView, MouseButton,
|
||||
MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
div, prelude::*, uniform_list, AnyElement, AppContext, Div, FocusHandle, FocusableView,
|
||||
MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle, View, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||
|
@ -16,7 +17,6 @@ pub struct Picker<D: PickerDelegate> {
|
|||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
type ListItem: IntoElement;
|
||||
|
||||
fn match_count(&self) -> usize;
|
||||
fn selected_index(&self) -> usize;
|
||||
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| {
|
||||
el.child(
|
||||
v_stack()
|
||||
.p_1()
|
||||
.grow()
|
||||
.child(
|
||||
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()
|
||||
.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 util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Prettier {
|
||||
Real(RealPrettier),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Test(TestPrettier),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealPrettier {
|
||||
default: bool,
|
||||
prettier_dir: PathBuf,
|
||||
|
@ -26,11 +28,13 @@ pub struct RealPrettier {
|
|||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone)]
|
||||
pub struct TestPrettier {
|
||||
prettier_dir: PathBuf,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
pub const FAIL_THRESHOLD: usize = 4;
|
||||
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||
|
|
|
@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
|
|||
const { method, id, params } = message;
|
||||
if (method === undefined) {
|
||||
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||
} else if (method == "initialized") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === undefined) {
|
||||
throw new Error(`Message id is undefined: ${JSON.stringify(message)}`);
|
||||
}
|
||||
|
|
|
@ -13,12 +13,14 @@ use std::{
|
|||
};
|
||||
use util::paths::{PathMatcher, DEFAULT_PRETTIER_DIR};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub enum Prettier {
|
||||
Real(RealPrettier),
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
Test(TestPrettier),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RealPrettier {
|
||||
default: bool,
|
||||
prettier_dir: PathBuf,
|
||||
|
@ -26,11 +28,13 @@ pub struct RealPrettier {
|
|||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
#[derive(Clone)]
|
||||
pub struct TestPrettier {
|
||||
prettier_dir: PathBuf,
|
||||
default: bool,
|
||||
}
|
||||
|
||||
pub const FAIL_THRESHOLD: usize = 4;
|
||||
pub const PRETTIER_SERVER_FILE: &str = "prettier_server.js";
|
||||
pub const PRETTIER_SERVER_JS: &str = include_str!("./prettier_server.js");
|
||||
const PRETTIER_PACKAGE_NAME: &str = "prettier";
|
||||
|
|
|
@ -153,7 +153,10 @@ async function handleMessage(message, prettier) {
|
|||
const { method, id, params } = message;
|
||||
if (method === undefined) {
|
||||
throw new Error(`Message method is undefined: ${JSON.stringify(message)}`);
|
||||
} else if (method == "initialized") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (id === undefined) {
|
||||
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 lsp_command;
|
||||
mod prettier_support;
|
||||
pub mod project_settings;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
|
@ -20,7 +21,7 @@ use futures::{
|
|||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
future::{self, try_join_all, Shared},
|
||||
future::{try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
|
@ -31,9 +32,7 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
||||
},
|
||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
|
@ -54,7 +53,7 @@ use lsp_command::*;
|
|||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
use prettier::Prettier;
|
||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
|
@ -72,7 +71,7 @@ use std::{
|
|||
hash::Hash,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{ControlFlow, Range},
|
||||
ops::Range,
|
||||
path::{self, Component, Path, PathBuf},
|
||||
process::Stdio,
|
||||
str,
|
||||
|
@ -85,11 +84,8 @@ use std::{
|
|||
use terminals::Terminals;
|
||||
use text::Anchor;
|
||||
use util::{
|
||||
debug_panic, defer,
|
||||
http::HttpClient,
|
||||
merge_json_value_into,
|
||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
||||
post_inc, ResultExt, TryFutureExt as _,
|
||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
|
@ -168,16 +164,9 @@ pub struct Project {
|
|||
copilot_log_subscription: Option<lsp::Subscription>,
|
||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||
node: Option<Arc<dyn NodeRuntime>>,
|
||||
default_prettier: Option<DefaultPrettier>,
|
||||
default_prettier: DefaultPrettier,
|
||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||
prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
||||
}
|
||||
|
||||
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>,
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
}
|
||||
|
||||
struct DelayedDebounced {
|
||||
|
@ -542,6 +531,14 @@ struct ProjectLspAdapterDelegate {
|
|||
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 {
|
||||
fn from_proto(value: i32) -> FormatTrigger {
|
||||
match value {
|
||||
|
@ -690,7 +687,7 @@ impl Project {
|
|||
copilot_log_subscription: None,
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
node: Some(node),
|
||||
default_prettier: None,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
}
|
||||
|
@ -791,7 +788,7 @@ impl Project {
|
|||
copilot_log_subscription: None,
|
||||
current_lsp_settings: settings::get::<ProjectSettings>(cx).lsp.clone(),
|
||||
node: None,
|
||||
default_prettier: None,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
};
|
||||
|
@ -928,8 +925,19 @@ impl Project {
|
|||
.detach();
|
||||
}
|
||||
|
||||
let mut prettier_plugins_by_worktree = HashMap::default();
|
||||
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.
|
||||
|
@ -2685,8 +2693,11 @@ impl Project {
|
|||
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||
|
||||
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
||||
if let Some(prettier_plugins) =
|
||||
prettier_support::prettier_plugins_for_language(&new_language, &settings)
|
||||
{
|
||||
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||
};
|
||||
if let Some(file) = buffer_file {
|
||||
let worktree = file.worktree.clone();
|
||||
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 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;
|
||||
|
||||
// First, format buffer's whitespace according to the settings.
|
||||
|
@ -4099,18 +4108,10 @@ impl Project {
|
|||
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
|
||||
// or external command.
|
||||
let mut format_operation = None;
|
||||
match (formatter, format_on_save) {
|
||||
match (&settings.formatter, &settings.format_on_save) {
|
||||
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
||||
|
||||
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
||||
|
@ -4155,46 +4156,11 @@ impl Project {
|
|||
}
|
||||
}
|
||||
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some((prettier_path, prettier_task)) = project
|
||||
.update(&mut cx, |project, 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
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
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:#}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(new_operation) =
|
||||
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||
.await
|
||||
{
|
||||
format_operation = Some(new_operation);
|
||||
} else if let Some((language_server, buffer_abs_path)) =
|
||||
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||
{
|
||||
|
@ -4212,48 +4178,13 @@ impl Project {
|
|||
));
|
||||
}
|
||||
}
|
||||
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some((prettier_path, prettier_task)) = project
|
||||
.update(&mut cx, |project, 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
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
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:#}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some(new_operation) =
|
||||
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||
.await
|
||||
{
|
||||
format_operation = Some(new_operation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -6566,85 +6497,6 @@ impl Project {
|
|||
.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>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
|
@ -8536,446 +8388,6 @@ impl Project {
|
|||
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(
|
||||
|
|
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 lsp_command;
|
||||
mod prettier_support;
|
||||
pub mod project_settings;
|
||||
pub mod search;
|
||||
pub mod terminals;
|
||||
|
@ -20,7 +21,7 @@ use futures::{
|
|||
mpsc::{self, UnboundedReceiver},
|
||||
oneshot,
|
||||
},
|
||||
future::{self, try_join_all, Shared},
|
||||
future::{try_join_all, Shared},
|
||||
stream::FuturesUnordered,
|
||||
AsyncWriteExt, Future, FutureExt, StreamExt, TryFutureExt,
|
||||
};
|
||||
|
@ -31,9 +32,7 @@ use gpui::{
|
|||
};
|
||||
use itertools::Itertools;
|
||||
use language::{
|
||||
language_settings::{
|
||||
language_settings, FormatOnSave, Formatter, InlayHintKind, LanguageSettings,
|
||||
},
|
||||
language_settings::{language_settings, FormatOnSave, Formatter, InlayHintKind},
|
||||
point_to_lsp,
|
||||
proto::{
|
||||
deserialize_anchor, deserialize_fingerprint, deserialize_line_ending, deserialize_version,
|
||||
|
@ -54,7 +53,7 @@ use lsp_command::*;
|
|||
use node_runtime::NodeRuntime;
|
||||
use parking_lot::Mutex;
|
||||
use postage::watch;
|
||||
use prettier::Prettier;
|
||||
use prettier_support::{DefaultPrettier, PrettierInstance};
|
||||
use project_settings::{LspSettings, ProjectSettings};
|
||||
use rand::prelude::*;
|
||||
use search::SearchQuery;
|
||||
|
@ -70,7 +69,7 @@ use std::{
|
|||
hash::Hash,
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{ControlFlow, Range},
|
||||
ops::Range,
|
||||
path::{self, Component, Path, PathBuf},
|
||||
process::Stdio,
|
||||
str,
|
||||
|
@ -83,11 +82,8 @@ use std::{
|
|||
use terminals::Terminals;
|
||||
use text::Anchor;
|
||||
use util::{
|
||||
debug_panic, defer,
|
||||
http::HttpClient,
|
||||
merge_json_value_into,
|
||||
paths::{DEFAULT_PRETTIER_DIR, LOCAL_SETTINGS_RELATIVE_PATH},
|
||||
post_inc, ResultExt, TryFutureExt as _,
|
||||
debug_panic, defer, http::HttpClient, merge_json_value_into,
|
||||
paths::LOCAL_SETTINGS_RELATIVE_PATH, post_inc, ResultExt, TryFutureExt as _,
|
||||
};
|
||||
|
||||
pub use fs::*;
|
||||
|
@ -166,16 +162,9 @@ pub struct Project {
|
|||
copilot_log_subscription: Option<lsp::Subscription>,
|
||||
current_lsp_settings: HashMap<Arc<str>, LspSettings>,
|
||||
node: Option<Arc<dyn NodeRuntime>>,
|
||||
default_prettier: Option<DefaultPrettier>,
|
||||
default_prettier: DefaultPrettier,
|
||||
prettiers_per_worktree: HashMap<WorktreeId, HashSet<Option<PathBuf>>>,
|
||||
prettier_instances: HashMap<PathBuf, Shared<Task<Result<Arc<Prettier>, Arc<anyhow::Error>>>>>,
|
||||
}
|
||||
|
||||
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>,
|
||||
prettier_instances: HashMap<PathBuf, PrettierInstance>,
|
||||
}
|
||||
|
||||
struct DelayedDebounced {
|
||||
|
@ -540,6 +529,14 @@ struct ProjectLspAdapterDelegate {
|
|||
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 {
|
||||
fn from_proto(value: i32) -> FormatTrigger {
|
||||
match value {
|
||||
|
@ -689,7 +686,7 @@ impl Project {
|
|||
copilot_log_subscription: None,
|
||||
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
||||
node: Some(node),
|
||||
default_prettier: None,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
}
|
||||
|
@ -792,7 +789,7 @@ impl Project {
|
|||
copilot_log_subscription: None,
|
||||
current_lsp_settings: ProjectSettings::get_global(cx).lsp.clone(),
|
||||
node: None,
|
||||
default_prettier: None,
|
||||
default_prettier: DefaultPrettier::default(),
|
||||
prettiers_per_worktree: HashMap::default(),
|
||||
prettier_instances: HashMap::default(),
|
||||
};
|
||||
|
@ -965,8 +962,19 @@ impl Project {
|
|||
.detach();
|
||||
}
|
||||
|
||||
let mut prettier_plugins_by_worktree = HashMap::default();
|
||||
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.
|
||||
|
@ -2722,8 +2730,11 @@ impl Project {
|
|||
let settings = language_settings(Some(&new_language), buffer_file.as_ref(), cx).clone();
|
||||
let buffer_file = File::from_dyn(buffer_file.as_ref());
|
||||
let worktree = buffer_file.as_ref().map(|f| f.worktree_id(cx));
|
||||
|
||||
self.install_default_formatters(worktree, &new_language, &settings, cx);
|
||||
if let Some(prettier_plugins) =
|
||||
prettier_support::prettier_plugins_for_language(&new_language, &settings)
|
||||
{
|
||||
self.install_default_prettier(worktree, prettier_plugins, cx);
|
||||
};
|
||||
if let Some(file) = buffer_file {
|
||||
let worktree = file.worktree.clone();
|
||||
if let Some(tree) = worktree.read(cx).as_local() {
|
||||
|
@ -4126,7 +4137,8 @@ impl Project {
|
|||
this.buffers_being_formatted
|
||||
.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 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;
|
||||
|
||||
// First, format buffer's whitespace according to the settings.
|
||||
|
@ -4164,18 +4174,10 @@ impl Project {
|
|||
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
|
||||
// or external command.
|
||||
let mut format_operation = None;
|
||||
match (formatter, format_on_save) {
|
||||
match (&settings.formatter, &settings.format_on_save) {
|
||||
(_, FormatOnSave::Off) if trigger == FormatTrigger::Save => {}
|
||||
|
||||
(Formatter::LanguageServer, FormatOnSave::On | FormatOnSave::Off)
|
||||
|
@ -4220,46 +4222,11 @@ impl Project {
|
|||
}
|
||||
}
|
||||
(Formatter::Auto, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some((prettier_path, prettier_task)) = project
|
||||
.update(&mut cx, |project, 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
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
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:#}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(new_operation) =
|
||||
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||
.await
|
||||
{
|
||||
format_operation = Some(new_operation);
|
||||
} else if let Some((language_server, buffer_abs_path)) =
|
||||
language_server.as_ref().zip(buffer_abs_path.as_ref())
|
||||
{
|
||||
|
@ -4277,48 +4244,13 @@ impl Project {
|
|||
));
|
||||
}
|
||||
}
|
||||
(Formatter::Prettier { .. }, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some((prettier_path, prettier_task)) = project
|
||||
.update(&mut cx, |project, 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
|
||||
.context("formatting via prettier")?,
|
||||
));
|
||||
}
|
||||
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:#}");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(Formatter::Prettier, FormatOnSave::On | FormatOnSave::Off) => {
|
||||
if let Some(new_operation) =
|
||||
prettier_support::format_with_prettier(&project, buffer, &mut cx)
|
||||
.await
|
||||
{
|
||||
format_operation = Some(new_operation);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -6638,84 +6570,6 @@ impl Project {
|
|||
.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>) {
|
||||
let new_active_entry = entry.and_then(|project_path| {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
|
@ -8579,486 +8433,6 @@ impl Project {
|
|||
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(
|
||||
|
|
|
@ -41,56 +41,47 @@ impl FileAssociations {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn get_icon(path: &Path, cx: &AppContext) -> Arc<str> {
|
||||
pub fn get_icon(path: &Path, cx: &AppContext) -> Option<Arc<str>> {
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
|
||||
// FIXME: Associate a type with the languages and have the file's langauge
|
||||
// override these associations
|
||||
maybe!({
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
let suffix = path.icon_suffix()?;
|
||||
|
||||
// FIXME: Associate a type with the languages and have the file's langauge
|
||||
// override these associations
|
||||
maybe!({
|
||||
let suffix = path.icon_suffix()?;
|
||||
|
||||
this.suffixes
|
||||
.get(suffix)
|
||||
.and_then(|type_str| this.types.get(type_str))
|
||||
.map(|type_config| type_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> {
|
||||
maybe!({
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
|
||||
let key = if expanded {
|
||||
EXPANDED_DIRECTORY_TYPE
|
||||
} else {
|
||||
COLLAPSED_DIRECTORY_TYPE
|
||||
};
|
||||
|
||||
this.types
|
||||
.get(key)
|
||||
this.suffixes
|
||||
.get(suffix)
|
||||
.and_then(|type_str| this.types.get(type_str))
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
})
|
||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
||||
.or_else(|| this.types.get("default").map(|config| config.icon.clone()))
|
||||
}
|
||||
|
||||
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Arc<str> {
|
||||
maybe!({
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
pub fn get_folder_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
|
||||
let key = if expanded {
|
||||
EXPANDED_CHEVRON_TYPE
|
||||
} else {
|
||||
COLLAPSED_CHEVRON_TYPE
|
||||
};
|
||||
let key = if expanded {
|
||||
EXPANDED_DIRECTORY_TYPE
|
||||
} else {
|
||||
COLLAPSED_DIRECTORY_TYPE
|
||||
};
|
||||
|
||||
this.types
|
||||
.get(key)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
})
|
||||
.unwrap_or_else(|| Arc::from("".to_string()))
|
||||
this.types
|
||||
.get(key)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
}
|
||||
|
||||
pub fn get_chevron_icon(expanded: bool, cx: &AppContext) -> Option<Arc<str>> {
|
||||
let this = cx.has_global::<Self>().then(|| cx.global::<Self>())?;
|
||||
|
||||
let key = if expanded {
|
||||
EXPANDED_CHEVRON_TYPE
|
||||
} else {
|
||||
COLLAPSED_CHEVRON_TYPE
|
||||
};
|
||||
|
||||
this.types
|
||||
.get(key)
|
||||
.map(|type_config| type_config.icon.clone())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1283,16 +1283,16 @@ impl ProjectPanel {
|
|||
let icon = match entry.kind {
|
||||
EntryKind::File(_) => {
|
||||
if show_file_icons {
|
||||
Some(FileAssociations::get_icon(&entry.path, cx))
|
||||
FileAssociations::get_icon(&entry.path, cx)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if show_folder_icons {
|
||||
Some(FileAssociations::get_folder_icon(is_expanded, cx))
|
||||
FileAssociations::get_folder_icon(is_expanded, cx)
|
||||
} 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 crate::mode::SearchMode;
|
||||
|
@ -6,7 +6,7 @@ use crate::mode::SearchMode;
|
|||
pub(super) fn render_nav_button(
|
||||
icon: ui::Icon,
|
||||
_active: bool,
|
||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> impl IntoElement {
|
||||
// let tooltip_style = cx.theme().tooltip.clone();
|
||||
// let cursor_style = if active {
|
||||
|
@ -21,7 +21,7 @@ pub(super) fn render_nav_button(
|
|||
pub(crate) fn render_search_mode_button(
|
||||
mode: SearchMode,
|
||||
is_active: bool,
|
||||
on_click: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
on_click: impl Fn(&ClickEvent, &mut WindowContext) + 'static,
|
||||
) -> Button {
|
||||
let button_variant = if is_active {
|
||||
ButtonVariant::Filled
|
||||
|
|
|
@ -2,7 +2,7 @@ use gpui::{
|
|||
actions, div, prelude::*, Div, FocusHandle, Focusable, KeyBinding, Render, Stateful, View,
|
||||
WindowContext,
|
||||
};
|
||||
use theme2::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
|
||||
actions!(ActionA, ActionB, ActionC);
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ use gpui::{
|
|||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use std::sync::Arc;
|
||||
use theme2::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::{Label, ListItem};
|
||||
|
||||
pub struct PickerStory {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext};
|
||||
use theme2::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::Tooltip;
|
||||
|
||||
pub struct ScrollStory;
|
||||
|
|
|
@ -19,7 +19,6 @@ pub enum ComponentStory {
|
|||
Focus,
|
||||
Icon,
|
||||
IconButton,
|
||||
Input,
|
||||
Keybinding,
|
||||
Label,
|
||||
ListItem,
|
||||
|
@ -39,7 +38,6 @@ impl ComponentStory {
|
|||
Self::Focus => FocusStory::view(cx).into(),
|
||||
Self::Icon => cx.build_view(|_| ui::IconStory).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::Label => cx.build_view(|_| ui::LabelStory).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> + '_ {
|
||||
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 button;
|
||||
mod button2;
|
||||
mod checkbox;
|
||||
mod context_menu;
|
||||
mod disclosure;
|
||||
mod divider;
|
||||
mod icon;
|
||||
mod icon_button;
|
||||
mod input;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list;
|
||||
|
@ -21,13 +21,13 @@ mod stories;
|
|||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use button2::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use disclosure::*;
|
||||
pub use divider::*;
|
||||
pub use icon::*;
|
||||
pub use icon_button::*;
|
||||
pub use input::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list::*;
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use std::rc::Rc;
|
||||
|
||||
use gpui::{
|
||||
DefiniteLength, Div, Hsla, IntoElement, MouseButton, MouseDownEvent,
|
||||
StatefulInteractiveElement, WindowContext,
|
||||
ClickEvent, DefiniteLength, Div, Hsla, IntoElement, StatefulInteractiveElement, WindowContext,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, Color, Icon, IconButton, IconElement, Label, LineHeightStyle};
|
||||
|
@ -67,7 +65,7 @@ impl ButtonVariant {
|
|||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
disabled: bool,
|
||||
click_handler: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext)>>,
|
||||
click_handler: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext)>>,
|
||||
icon: Option<Icon>,
|
||||
icon_position: Option<IconPosition>,
|
||||
label: SharedString,
|
||||
|
@ -118,7 +116,7 @@ impl RenderOnce for Button {
|
|||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
@ -168,10 +166,7 @@ impl Button {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
|
||||
self.click_handler = Some(Rc::new(handler));
|
||||
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()) {
|
||||
(false, _) => div(),
|
||||
(_, true) => div().child(
|
||||
IconElement::new(Icon::ChevronDown)
|
||||
IconButton::new("toggle", Icon::ChevronDown)
|
||||
.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(
|
||||
IconElement::new(Icon::ChevronRight)
|
||||
IconButton::new("toggle", Icon::ChevronRight)
|
||||
.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 gpui::{prelude::*, Action, AnyView, Div, MouseButton, MouseDownEvent, Stateful};
|
||||
use crate::{h_stack, prelude::*, Icon, IconElement, IconSize};
|
||||
use gpui::{prelude::*, Action, AnyView, ClickEvent, Div, Stateful};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
id: ElementId,
|
||||
icon: Icon,
|
||||
color: Color,
|
||||
size: IconSize,
|
||||
variant: ButtonVariant,
|
||||
state: InteractionState,
|
||||
disabled: bool,
|
||||
selected: bool,
|
||||
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 {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
let icon_color = match (self.state, self.color) {
|
||||
(InteractionState::Disabled, _) => Color::Disabled,
|
||||
(InteractionState::Active, _) => Color::Selected,
|
||||
let icon_color = match (self.disabled, self.selected, self.color) {
|
||||
(true, _, _) => Color::Disabled,
|
||||
(false, true, _) => Color::Selected,
|
||||
_ => self.color,
|
||||
};
|
||||
|
||||
|
@ -50,10 +51,14 @@ impl RenderOnce for IconButton {
|
|||
// place we use an icon button.
|
||||
// .hover(|style| style.bg(bg_hover_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 {
|
||||
button = button.on_mouse_down(MouseButton::Left, move |event, cx| {
|
||||
if let Some(click_handler) = self.on_click {
|
||||
button = button.on_click(move |event, cx| {
|
||||
cx.stop_propagation();
|
||||
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.
|
||||
div().id(self.id.clone()).child(button)
|
||||
button
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,11 +80,12 @@ impl IconButton {
|
|||
id: id.into(),
|
||||
icon,
|
||||
color: Color::default(),
|
||||
size: Default::default(),
|
||||
variant: ButtonVariant::default(),
|
||||
state: InteractionState::default(),
|
||||
selected: false,
|
||||
disabled: false,
|
||||
tooltip: None,
|
||||
on_mouse_down: None,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,13 +99,13 @@ impl IconButton {
|
|||
self
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn state(mut self, state: InteractionState) -> Self {
|
||||
self.state = state;
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
|
@ -109,16 +114,18 @@ impl IconButton {
|
|||
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 {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl 'static + Fn(&MouseDownEvent, &mut WindowContext),
|
||||
) -> Self {
|
||||
self.on_mouse_down = Some(Box::new(handler));
|
||||
pub fn on_click(mut self, handler: impl 'static + Fn(&ClickEvent, &mut WindowContext)) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
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>,
|
||||
meta: Option<ListHeaderMeta>,
|
||||
toggle: Toggle,
|
||||
on_toggle: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
inset: bool,
|
||||
selected: bool,
|
||||
}
|
||||
|
||||
impl ListHeader {
|
||||
|
@ -36,6 +38,8 @@ impl ListHeader {
|
|||
meta: None,
|
||||
inset: false,
|
||||
toggle: Toggle::NotToggleable,
|
||||
on_toggle: None,
|
||||
selected: false,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +48,14 @@ impl ListHeader {
|
|||
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 {
|
||||
self.left_icon = left_icon;
|
||||
self
|
||||
|
@ -57,13 +69,18 @@ impl ListHeader {
|
|||
self.meta = meta;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected(mut self, selected: bool) -> Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for ListHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
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 {
|
||||
Some(ListHeaderMeta::Tools(icons)) => div().child(
|
||||
|
@ -85,6 +102,9 @@ impl RenderOnce for ListHeader {
|
|||
div()
|
||||
.h_5()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.flex()
|
||||
.flex_1()
|
||||
.items_center()
|
||||
|
@ -177,6 +197,7 @@ pub struct ListItem {
|
|||
toggle: Toggle,
|
||||
inset: bool,
|
||||
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>>,
|
||||
children: SmallVec<[AnyElement; 2]>,
|
||||
}
|
||||
|
@ -193,6 +214,7 @@ impl ListItem {
|
|||
inset: false,
|
||||
on_click: None,
|
||||
on_secondary_mouse_down: None,
|
||||
on_toggle: None,
|
||||
children: SmallVec::new(),
|
||||
}
|
||||
}
|
||||
|
@ -230,6 +252,14 @@ impl ListItem {
|
|||
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 {
|
||||
self.selected = selected;
|
||||
self
|
||||
|
@ -255,19 +285,6 @@ impl RenderOnce for ListItem {
|
|||
type Rendered = Stateful<Div>;
|
||||
|
||||
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()
|
||||
.id(self.id)
|
||||
.relative()
|
||||
|
@ -282,8 +299,8 @@ impl RenderOnce for ListItem {
|
|||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
.when_some(self.on_click.clone(), |this, on_click| {
|
||||
this.on_click(move |event, cx| {
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(move |event, cx| {
|
||||
// HACK: GPUI currently fires `on_click` with any mouse button,
|
||||
// but we only care about the left button.
|
||||
if event.down.button == MouseButton::Left {
|
||||
|
@ -304,23 +321,18 @@ impl RenderOnce for ListItem {
|
|||
.gap_1()
|
||||
.items_center()
|
||||
.relative()
|
||||
.child(disclosure_control(self.toggle))
|
||||
.children(left_content)
|
||||
.children(self.children)
|
||||
// HACK: We need to attach the `on_click` handler to the child element in order to have the click
|
||||
// event actually fire.
|
||||
// Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
|
||||
// outer `div`.
|
||||
.id("on_click_hack")
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.on_click(move |event, cx| {
|
||||
// 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)
|
||||
}
|
||||
})
|
||||
}),
|
||||
.child(disclosure_control(self.toggle, self.on_toggle))
|
||||
.map(|this| match self.left_slot {
|
||||
Some(GraphicSlot::Icon(i)) => this.child(
|
||||
IconElement::new(i)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)),
|
||||
Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)),
|
||||
None => this,
|
||||
})
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,18 +4,15 @@ mod checkbox;
|
|||
mod context_menu;
|
||||
mod icon;
|
||||
mod icon_button;
|
||||
mod input;
|
||||
mod keybinding;
|
||||
mod label;
|
||||
mod list_item;
|
||||
|
||||
pub use avatar::*;
|
||||
pub use button::*;
|
||||
pub use checkbox::*;
|
||||
pub use context_menu::*;
|
||||
pub use icon::*;
|
||||
pub use icon_button::*;
|
||||
pub use input::*;
|
||||
pub use keybinding::*;
|
||||
pub use label::*;
|
||||
pub use list_item::*;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
use gpui::{rems, Div, Render};
|
||||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{h_stack, v_stack, Button, Icon, IconPosition, Label};
|
||||
use crate::{h_stack, Button, Icon, IconPosition};
|
||||
|
||||
pub struct ButtonStory;
|
||||
|
||||
|
@ -11,8 +10,6 @@ impl Render for ButtonStory {
|
|||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
let states = InteractionState::iter();
|
||||
|
||||
Story::container()
|
||||
.child(Story::title_for::<Button>())
|
||||
.child(
|
||||
|
@ -20,121 +17,56 @@ impl Render for ButtonStory {
|
|||
.flex()
|
||||
.gap_8()
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Ghost (Default)"))
|
||||
.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::Ghost), // .state(state),
|
||||
)
|
||||
})))
|
||||
.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(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.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(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
div().child(Story::label("Ghost (Default)")).child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Button::new("Label").variant(ButtonVariant::Ghost)),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Ghost – Left Icon"))
|
||||
.child(
|
||||
div()
|
||||
.child(Story::label("Filled"))
|
||||
.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),
|
||||
)
|
||||
})))
|
||||
.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(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left), // .state(state),
|
||||
)
|
||||
})))
|
||||
.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(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right), // .state(state),
|
||||
)
|
||||
}))),
|
||||
)
|
||||
.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())),
|
||||
)
|
||||
}))),
|
||||
h_stack().gap_2().child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Ghost – Right Icon"))
|
||||
.child(
|
||||
h_stack().gap_2().child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Ghost)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().child(Story::label("Filled")).child(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Button::new("Label").variant(ButtonVariant::Filled)),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Filled – Left Button"))
|
||||
.child(
|
||||
h_stack().gap_2().child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Left),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Filled – Right Button"))
|
||||
.child(
|
||||
h_stack().gap_2().child(
|
||||
Button::new("Label")
|
||||
.variant(ButtonVariant::Filled)
|
||||
.icon(Icon::Plus)
|
||||
.icon_position(IconPosition::Right),
|
||||
),
|
||||
)
|
||||
.child(Story::label("Button with `on_click`"))
|
||||
.child(
|
||||
Button::new("Label")
|
||||
|
|
|
@ -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 crate::prelude::*;
|
||||
use crate::ListItem;
|
||||
use crate::{Icon, ListItem};
|
||||
|
||||
pub struct ListItemStory;
|
||||
|
||||
|
@ -14,6 +14,20 @@ impl Render for ListItemStory {
|
|||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.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(
|
||||
ListItem::new("with_on_click")
|
||||
|
@ -24,11 +38,11 @@ impl Render for ListItemStory {
|
|||
)
|
||||
.child(Story::label("With `on_secondary_mouse_down`"))
|
||||
.child(
|
||||
ListItem::new("with_on_secondary_mouse_down").on_secondary_mouse_down(
|
||||
|_event, _cx| {
|
||||
ListItem::new("with_on_secondary_mouse_down")
|
||||
.child("Right click me")
|
||||
.on_secondary_mouse_down(|_event, _cx| {
|
||||
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,
|
||||
};
|
||||
|
||||
pub use crate::clickable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::StyledExt;
|
||||
pub use crate::{ButtonVariant, Color};
|
||||
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 theme::ActiveTheme;
|
||||
|
||||
#[derive(Default, PartialEq, Copy, Clone)]
|
||||
#[derive(Debug, Default, PartialEq, Copy, Clone)]
|
||||
pub enum Color {
|
||||
#[default]
|
||||
Default,
|
||||
|
|
|
@ -12,13 +12,19 @@
|
|||
#![doc = include_str!("../docs/building-ui.md")]
|
||||
#![doc = include_str!("../docs/todo.md")]
|
||||
|
||||
mod clickable;
|
||||
mod components;
|
||||
mod fixed;
|
||||
pub mod prelude;
|
||||
mod selectable;
|
||||
mod styled_ext;
|
||||
mod styles;
|
||||
pub mod utils;
|
||||
|
||||
pub use clickable::*;
|
||||
pub use components::*;
|
||||
pub use fixed::*;
|
||||
pub use prelude::*;
|
||||
pub use selectable::*;
|
||||
pub use styled_ext::*;
|
||||
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 serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use theme2::ActiveTheme;
|
||||
use ui::{h_stack, menu_handle, ContextMenu, IconButton, InteractionState, Tooltip};
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, menu_handle, ContextMenu, IconButton, Tooltip};
|
||||
|
||||
pub enum PanelEvent {
|
||||
ChangePosition,
|
||||
|
@ -686,22 +686,26 @@ impl Render for PanelButtons {
|
|||
let name = entry.panel.persistent_name();
|
||||
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 tooltip: SharedString =
|
||||
format!("Close {} dock", dock.position.to_label()).into();
|
||||
IconButton::new(name, icon)
|
||||
.state(InteractionState::Active)
|
||||
.action(action.boxed_clone())
|
||||
.tooltip(move |cx| Tooltip::for_action(tooltip.clone(), &*action, cx))
|
||||
|
||||
(action, tooltip)
|
||||
} else {
|
||||
let action = entry.panel.toggle_action(cx);
|
||||
|
||||
IconButton::new(name, icon)
|
||||
.action(action.boxed_clone())
|
||||
.tooltip(move |cx| Tooltip::for_action(name, &*action, cx))
|
||||
(action, name.into())
|
||||
};
|
||||
|
||||
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(
|
||||
menu_handle(name)
|
||||
.menu(move |cx| {
|
||||
|
|
|
@ -1482,18 +1482,14 @@ impl Pane {
|
|||
.gap_px()
|
||||
.child(
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new("navigate_backward", Icon::ArrowLeft).state(
|
||||
InteractionState::Enabled
|
||||
.if_enabled(self.can_navigate_backward()),
|
||||
),
|
||||
IconButton::new("navigate_backward", Icon::ArrowLeft)
|
||||
.disabled(!self.can_navigate_backward()),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight).state(
|
||||
InteractionState::Enabled
|
||||
.if_enabled(self.can_navigate_forward()),
|
||||
),
|
||||
IconButton::new("navigate_forward", Icon::ArrowRight)
|
||||
.disabled(!self.can_navigate_forward()),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
@ -5,7 +5,7 @@ use gpui::{
|
|||
div, AnyView, Div, IntoElement, ParentElement, Render, Styled, Subscription, View, ViewContext,
|
||||
WindowContext,
|
||||
};
|
||||
use theme2::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, Button, Icon, IconButton};
|
||||
use util::ResultExt;
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ use gpui::{
|
|||
div, AnyView, Div, Entity, EntityId, EventEmitter, ParentElement as _, Render, Styled, View,
|
||||
ViewContext, WindowContext,
|
||||
};
|
||||
use theme2::ActiveTheme;
|
||||
use ui::prelude::*;
|
||||
use ui::{h_stack, v_stack, Button, Color, Icon, IconButton, Label};
|
||||
|
||||
pub enum ToolbarItemEvent {
|
||||
|
|
|
@ -1808,22 +1808,22 @@ impl Workspace {
|
|||
pane
|
||||
}
|
||||
|
||||
// pub fn add_item_to_center(
|
||||
// &mut self,
|
||||
// item: Box<dyn ItemHandle>,
|
||||
// cx: &mut ViewContext<Self>,
|
||||
// ) -> bool {
|
||||
// if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||
// if let Some(center_pane) = center_pane.upgrade(cx) {
|
||||
// center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
||||
// true
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// } else {
|
||||
// false
|
||||
// }
|
||||
// }
|
||||
pub fn add_item_to_center(
|
||||
&mut self,
|
||||
item: Box<dyn ItemHandle>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if let Some(center_pane) = self.last_active_center_pane.clone() {
|
||||
if let Some(center_pane) = center_pane.upgrade() {
|
||||
center_pane.update(cx, |pane, cx| pane.add_item(item, true, true, None, cx));
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_item(&mut self, item: Box<dyn ItemHandle>, cx: &mut ViewContext<Self>) {
|
||||
self.active_pane
|
||||
|
|
|
@ -3,7 +3,7 @@ authors = ["Nathan Sobo <nathansobo@gmail.com>"]
|
|||
description = "The fast, collaborative code editor."
|
||||
edition = "2021"
|
||||
name = "zed"
|
||||
version = "0.115.0"
|
||||
version = "0.116.0"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
|
|
|
@ -66,12 +66,12 @@ shellexpand = "2.1.0"
|
|||
text = { package = "text2", path = "../text2" }
|
||||
terminal_view = { package = "terminal_view2", path = "../terminal_view2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
# theme_selector = { path = "../theme_selector" }
|
||||
theme_selector = { package = "theme_selector2", path = "../theme_selector2" }
|
||||
util = { path = "../util" }
|
||||
# semantic_index = { path = "../semantic_index" }
|
||||
# vim = { path = "../vim" }
|
||||
workspace = { package = "workspace2", path = "../workspace2" }
|
||||
# welcome = { path = "../welcome" }
|
||||
welcome = { package = "welcome2", path = "../welcome2" }
|
||||
zed_actions = {package = "zed_actions2", path = "../zed_actions2"}
|
||||
anyhow.workspace = true
|
||||
async-compression = { version = "0.3", features = ["gzip", "futures-bufread"] }
|
||||
|
|
|
@ -13,7 +13,7 @@ use db::kvp::KEY_VALUE_STORE;
|
|||
use editor::Editor;
|
||||
use fs::RealFs;
|
||||
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 language::LanguageRegistry;
|
||||
use log::LevelFilter;
|
||||
|
@ -36,7 +36,7 @@ use std::{
|
|||
path::{Path, PathBuf},
|
||||
sync::{
|
||||
atomic::{AtomicU32, Ordering},
|
||||
Arc,
|
||||
Arc, Weak,
|
||||
},
|
||||
thread,
|
||||
};
|
||||
|
@ -48,6 +48,7 @@ use util::{
|
|||
paths, ResultExt,
|
||||
};
|
||||
use uuid::Uuid;
|
||||
use welcome::{show_welcome_experience, FIRST_OPEN};
|
||||
use workspace::{AppState, WorkspaceStore};
|
||||
use zed2::{
|
||||
build_window_options, ensure_only_instance, handle_cli_connection, initialize_workspace,
|
||||
|
@ -103,16 +104,15 @@ fn main() {
|
|||
let listener = Arc::new(listener);
|
||||
let open_listener = listener.clone();
|
||||
app.on_open_urls(move |urls, _| open_listener.open_urls(&urls));
|
||||
app.on_reopen(move |_cx| {
|
||||
// todo!("workspace")
|
||||
// if cx.has_global::<Weak<AppState>>() {
|
||||
// if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
|
||||
// workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
// Editor::new_file(workspace, &Default::default(), cx)
|
||||
// })
|
||||
// .detach();
|
||||
// }
|
||||
// }
|
||||
app.on_reopen(move |cx| {
|
||||
if cx.has_global::<Weak<AppState>>() {
|
||||
if let Some(app_state) = cx.global::<Weak<AppState>>().upgrade() {
|
||||
workspace::open_new(&app_state, cx, |workspace, cx| {
|
||||
Editor::new_file(workspace, &Default::default(), cx)
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
app.run(move |cx| {
|
||||
|
@ -164,17 +164,16 @@ fn main() {
|
|||
// assistant::init(cx);
|
||||
// component_test::init(cx);
|
||||
|
||||
// cx.spawn(|cx| watch_themes(fs.clone(), cx)).detach();
|
||||
// cx.spawn(|_| watch_languages(fs.clone(), languages.clone()))
|
||||
// .detach();
|
||||
// watch_file_types(fs.clone(), cx);
|
||||
watch_file_types(fs.clone(), cx);
|
||||
|
||||
languages.set_theme(cx.theme().clone());
|
||||
// cx.observe_global::<SettingsStore, _>({
|
||||
// let languages = languages.clone();
|
||||
// move |cx| languages.set_theme(theme::current(cx).clone())
|
||||
// })
|
||||
// .detach();
|
||||
cx.observe_global::<SettingsStore>({
|
||||
let languages = languages.clone();
|
||||
move |cx| languages.set_theme(cx.theme().clone())
|
||||
})
|
||||
.detach();
|
||||
|
||||
client.telemetry().start(installation_id, session_id, cx);
|
||||
let telemetry_settings = *client::TelemetrySettings::get_global(cx);
|
||||
|
@ -193,7 +192,6 @@ fn main() {
|
|||
fs,
|
||||
build_window_options,
|
||||
call_factory: call::Call::new,
|
||||
// background_actions: todo!("ask Mikayla"),
|
||||
workspace_store,
|
||||
node_runtime,
|
||||
});
|
||||
|
@ -219,14 +217,13 @@ fn main() {
|
|||
|
||||
// journal2::init(app_state.clone(), cx);
|
||||
// language_selector::init(cx);
|
||||
// theme_selector::init(cx);
|
||||
theme_selector::init(cx);
|
||||
// activity_indicator::init(cx);
|
||||
// language_tools::init(cx);
|
||||
call::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
collab_ui::init(&app_state, cx);
|
||||
// feedback::init(cx);
|
||||
// welcome::init(cx);
|
||||
// zed::init(&app_state, cx);
|
||||
welcome::init(cx);
|
||||
|
||||
// cx.set_menus(menus::menus());
|
||||
initialize_workspace(app_state.clone(), cx);
|
||||
|
@ -279,17 +276,18 @@ fn main() {
|
|||
.detach();
|
||||
}
|
||||
Ok(Some(OpenRequest::JoinChannel { channel_id: _ })) => {
|
||||
todo!()
|
||||
// triggered_authentication = true;
|
||||
// let app_state = app_state.clone();
|
||||
// let client = client.clone();
|
||||
// cx.spawn(|mut cx| async move {
|
||||
// // ignore errors here, we'll show a generic "not signed in"
|
||||
// let _ = authenticate(client, &cx).await;
|
||||
// cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
|
||||
// .await
|
||||
// })
|
||||
// .detach_and_log_err(cx)
|
||||
triggered_authentication = true;
|
||||
let app_state = app_state.clone();
|
||||
let client = client.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
// ignore errors here, we'll show a generic "not signed in"
|
||||
let _ = authenticate(client, &cx).await;
|
||||
//todo!()
|
||||
// cx.update(|cx| workspace::join_channel(channel_id, app_state, None, cx))
|
||||
// .await
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx)
|
||||
}
|
||||
Ok(Some(OpenRequest::OpenChannelNotes { channel_id: _ })) => {
|
||||
todo!()
|
||||
|
@ -340,7 +338,7 @@ async fn authenticate(client: Arc<Client>, cx: &AsyncAppContext) -> Result<()> {
|
|||
if client::IMPERSONATE_LOGIN.is_some() {
|
||||
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?;
|
||||
}
|
||||
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))?
|
||||
.await
|
||||
.log_err();
|
||||
// todo!(welcome)
|
||||
//} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
//todo!()
|
||||
// cx.update(|cx| show_welcome_experience(app_state, cx));
|
||||
} else if matches!(KEY_VALUE_STORE.read_kvp(FIRST_OPEN), Ok(None)) {
|
||||
cx.update(|cx| show_welcome_experience(app_state, cx))
|
||||
.log_err();
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
workspace::open_new(app_state, cx, |workspace, cx| {
|
||||
|
@ -709,84 +706,49 @@ fn load_embedded_fonts(cx: &AppContext) {
|
|||
.unwrap();
|
||||
}
|
||||
|
||||
// #[cfg(debug_assertions)]
|
||||
// async fn watch_themes(fs: Arc<dyn Fs>, mut cx: AsyncAppContext) -> Option<()> {
|
||||
// let mut events = fs
|
||||
// .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)]
|
||||
async fn watch_languages(fs: Arc<dyn fs::Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
|
||||
use std::time::Duration;
|
||||
|
||||
// #[cfg(debug_assertions)]
|
||||
// async fn watch_languages(fs: Arc<dyn Fs>, languages: Arc<LanguageRegistry>) -> Option<()> {
|
||||
// let mut events = fs
|
||||
// .watch(
|
||||
// "crates/zed/src/languages".as_ref(),
|
||||
// Duration::from_millis(100),
|
||||
// )
|
||||
// .await;
|
||||
// while (events.next().await).is_some() {
|
||||
// languages.reload();
|
||||
// }
|
||||
// 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!()
|
||||
&[]
|
||||
let mut events = fs
|
||||
.watch(
|
||||
"crates/zed2/src/languages".as_ref(),
|
||||
Duration::from_millis(100),
|
||||
)
|
||||
.await;
|
||||
while (events.next().await).is_some() {
|
||||
languages.reload();
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
#[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 \
|
||||
--workspace-only \
|
||||
--offline \
|
||||
--root=zed,cli,collab \
|
||||
--root=zed2,cli,collab2 \
|
||||
--dedup-transitive-deps \
|
||||
| dot -Tsvg > $graph_file
|
||||
|
||||
|
|
Loading…
Reference in a new issue