mirror of
https://github.com/zed-industries/zed.git
synced 2024-12-28 04:02:54 +03:00
Merge branch 'main' into fix-splits-bugs
This commit is contained in:
commit
d88fc27b79
25
Cargo.lock
generated
25
Cargo.lock
generated
@ -6510,6 +6510,7 @@ dependencies = [
|
||||
"theme2",
|
||||
"ui2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@ -7019,6 +7020,29 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "project_symbols2"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"editor2",
|
||||
"futures 0.3.28",
|
||||
"fuzzy2",
|
||||
"gpui2",
|
||||
"language2",
|
||||
"lsp2",
|
||||
"ordered-float 2.10.0",
|
||||
"picker2",
|
||||
"postage",
|
||||
"project2",
|
||||
"settings2",
|
||||
"smol",
|
||||
"text2",
|
||||
"theme2",
|
||||
"util",
|
||||
"workspace2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus"
|
||||
version = "0.13.3"
|
||||
@ -12081,6 +12105,7 @@ dependencies = [
|
||||
"postage",
|
||||
"project2",
|
||||
"project_panel2",
|
||||
"project_symbols2",
|
||||
"quick_action_bar2",
|
||||
"rand 0.8.5",
|
||||
"recent_projects2",
|
||||
|
@ -90,6 +90,7 @@ members = [
|
||||
"crates/project_panel",
|
||||
"crates/project_panel2",
|
||||
"crates/project_symbols",
|
||||
"crates/project_symbols2",
|
||||
"crates/quick_action_bar2",
|
||||
"crates/recent_projects",
|
||||
"crates/recent_projects2",
|
||||
|
@ -21,10 +21,7 @@ use settings::{Settings, SettingsStore};
|
||||
use std::sync::Arc;
|
||||
use theme::ActiveTheme as _;
|
||||
use time::{OffsetDateTime, UtcOffset};
|
||||
use ui::{
|
||||
h_stack, prelude::WindowContext, v_stack, Avatar, Button, ButtonCommon as _, Clickable, Icon,
|
||||
IconButton, Label, Tooltip,
|
||||
};
|
||||
use ui::{prelude::*, Avatar, Button, Icon, IconButton, Label, Tooltip};
|
||||
use util::{ResultExt, TryFutureExt};
|
||||
use workspace::{
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
@ -382,13 +379,18 @@ impl ChatPanel {
|
||||
.child(text.element("body".into(), cx))
|
||||
.child(
|
||||
div()
|
||||
.invisible()
|
||||
.absolute()
|
||||
.top_1()
|
||||
.right_2()
|
||||
.w_8()
|
||||
.group_hover("", |this| this.visible())
|
||||
.child(render_remove(message_id_to_remove, cx)),
|
||||
.visible_on_hover("")
|
||||
.children(message_id_to_remove.map(|message_id| {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle).on_click(
|
||||
cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
@ -528,18 +530,6 @@ impl ChatPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn render_remove(message_id_to_remove: Option<u64>, cx: &mut ViewContext<ChatPanel>) -> AnyElement {
|
||||
if let Some(message_id) = message_id_to_remove {
|
||||
IconButton::new(("remove", message_id), Icon::XCircle)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.remove_message(message_id, cx);
|
||||
}))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<Event> for ChatPanel {}
|
||||
|
||||
impl Render for ChatPanel {
|
||||
|
@ -176,11 +176,11 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems,
|
||||
serde_json, size, Action, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent,
|
||||
Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, InteractiveElement,
|
||||
IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, Quad,
|
||||
Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, Subscription, Task,
|
||||
View, ViewContext, VisualContext, WeakView,
|
||||
serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem,
|
||||
DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla,
|
||||
InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point,
|
||||
PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled,
|
||||
Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
};
|
||||
use project::{Fs, Project};
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
@ -402,7 +402,7 @@ impl CollabPanel {
|
||||
|
||||
let filter_editor = cx.build_view(|cx| {
|
||||
let mut editor = Editor::single_line(cx);
|
||||
editor.set_placeholder_text("Filter channels, contacts", cx);
|
||||
editor.set_placeholder_text("Filter...", cx);
|
||||
editor
|
||||
});
|
||||
|
||||
@ -1156,25 +1156,21 @@ impl CollabPanel {
|
||||
let tooltip = format!("Follow {}", user.github_login);
|
||||
|
||||
ListItem::new(SharedString::from(user.github_login.clone()))
|
||||
.left_child(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.child(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", Icon::ArrowRight)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
}),
|
||||
)
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
.child(Label::new(user.github_login.clone()))
|
||||
.end_slot(if is_pending {
|
||||
Label::new("Calling").color(Color::Muted).into_any_element()
|
||||
} else if is_current_user {
|
||||
IconButton::new("leave-call", Icon::Exit)
|
||||
.style(ButtonStyle::Subtle)
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
Self::leave_call(cx);
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Leave Call", cx))
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
})
|
||||
.when_some(peer_id, |this, peer_id| {
|
||||
this.tooltip(move |cx| Tooltip::text(tooltip.clone(), cx))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
@ -1212,8 +1208,12 @@ impl CollabPanel {
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}))
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Folder))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Folder)),
|
||||
)
|
||||
.child(Label::new(project_name.clone()))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Open {}", project_name), cx))
|
||||
|
||||
@ -1305,8 +1305,12 @@ impl CollabPanel {
|
||||
let id = peer_id.map_or(usize::MAX, |id| id.as_u64() as usize);
|
||||
|
||||
ListItem::new(("screen", id))
|
||||
.left_child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Screen))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(is_last, cx))
|
||||
.child(IconButton::new(0, Icon::Screen)),
|
||||
)
|
||||
.child(Label::new("Screen"))
|
||||
.when_some(peer_id, |this, _| {
|
||||
this.on_click(cx.listener(move |this, _, cx| {
|
||||
@ -1372,9 +1376,13 @@ impl CollabPanel {
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::File))
|
||||
.child(Label::new("notes"))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::File)),
|
||||
)
|
||||
.child(div().h_7().w_full().child(Label::new("notes")))
|
||||
.tooltip(move |cx| Tooltip::text("Open Channel Notes", cx))
|
||||
}
|
||||
|
||||
@ -1387,8 +1395,12 @@ impl CollabPanel {
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx);
|
||||
}))
|
||||
.left_child(render_tree_branch(true, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles))
|
||||
.start_slot(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.child(render_tree_branch(false, cx))
|
||||
.child(IconButton::new(0, Icon::MessageBubbles)),
|
||||
)
|
||||
.child(Label::new("chat"))
|
||||
.tooltip(move |cx| Tooltip::text("Open Chat", cx))
|
||||
}
|
||||
@ -2149,11 +2161,6 @@ impl CollabPanel {
|
||||
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
|
||||
v_stack()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.p_2()
|
||||
.child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
|
||||
)
|
||||
.child(
|
||||
v_stack()
|
||||
.size_full()
|
||||
@ -2223,6 +2230,14 @@ impl CollabPanel {
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(
|
||||
div().p_2().child(
|
||||
div()
|
||||
.border_primary(cx)
|
||||
.border_t()
|
||||
.child(self.filter_editor.clone()),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
@ -2275,21 +2290,27 @@ impl CollabPanel {
|
||||
Section::ActiveCall => channel_link.map(|channel_link| {
|
||||
let channel_link_copy = channel_link.clone();
|
||||
IconButton::new("channel-link", Icon::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.size(ButtonSize::None)
|
||||
.visible_on_hover("section-header")
|
||||
.on_click(move |_, cx| {
|
||||
let item = ClipboardItem::new(channel_link_copy.clone());
|
||||
cx.write_to_clipboard(item)
|
||||
})
|
||||
.tooltip(|cx| Tooltip::text("Copy channel link", cx))
|
||||
.into_any_element()
|
||||
}),
|
||||
Section::Contacts => Some(
|
||||
IconButton::new("add-contact", Icon::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx)),
|
||||
.tooltip(|cx| Tooltip::text("Search for new contact", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
Section::Channels => Some(
|
||||
IconButton::new("add-channel", Icon::Plus)
|
||||
.on_click(cx.listener(|this, _, cx| this.new_root_channel(cx)))
|
||||
.tooltip(|cx| Tooltip::text("Create a channel", cx)),
|
||||
.tooltip(|cx| Tooltip::text("Create a channel", cx))
|
||||
.into_any_element(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
@ -2304,25 +2325,18 @@ impl CollabPanel {
|
||||
|
||||
h_stack()
|
||||
.w_full()
|
||||
.map(|el| {
|
||||
if can_collapse {
|
||||
el.child(
|
||||
ListItem::new(text.clone())
|
||||
.child(div().w_full().child(Label::new(text)))
|
||||
.selected(is_selected)
|
||||
.toggle(Some(!is_collapsed))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.toggle_section_expanded(section, cx)
|
||||
})),
|
||||
)
|
||||
} else {
|
||||
el.child(
|
||||
ListHeader::new(text)
|
||||
.when_some(button, |el, button| el.meta(button))
|
||||
.selected(is_selected),
|
||||
)
|
||||
}
|
||||
})
|
||||
.group("section-header")
|
||||
.child(
|
||||
ListHeader::new(text)
|
||||
.toggle(if can_collapse {
|
||||
Some(!is_collapsed)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
.inset(true)
|
||||
.end_slot::<AnyElement>(button)
|
||||
.selected(is_selected),
|
||||
)
|
||||
.when(section == Section::Channels, |el| {
|
||||
el.drag_over::<DraggedChannelView>(|style| {
|
||||
style.bg(cx.theme().colors().ghost_element_hover)
|
||||
@ -2363,25 +2377,20 @@ impl CollabPanel {
|
||||
})
|
||||
.when(!calling, |el| {
|
||||
el.child(
|
||||
div()
|
||||
.id("remove_contact")
|
||||
.invisible()
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new("remove_contact", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||
.on_click(cx.listener({
|
||||
let github_login = github_login.clone();
|
||||
move |this, _, cx| {
|
||||
this.remove_contact(user_id, &github_login, cx);
|
||||
}
|
||||
})),
|
||||
),
|
||||
IconButton::new("remove_contact", Icon::Close)
|
||||
.icon_color(Color::Muted)
|
||||
.visible_on_hover("")
|
||||
.tooltip(|cx| Tooltip::text("Remove Contact", cx))
|
||||
.on_click(cx.listener({
|
||||
let github_login = github_login.clone();
|
||||
move |this, _, cx| {
|
||||
this.remove_contact(user_id, &github_login, cx);
|
||||
}
|
||||
})),
|
||||
)
|
||||
}),
|
||||
)
|
||||
.left_child(
|
||||
.start_slot(
|
||||
// todo!() handle contacts with no avatar
|
||||
Avatar::new(contact.user.avatar_uri.clone())
|
||||
.availability_indicator(if online { Some(!busy) } else { None }),
|
||||
@ -2460,7 +2469,7 @@ impl CollabPanel {
|
||||
.child(Label::new(github_login.clone()))
|
||||
.child(h_stack().children(controls)),
|
||||
)
|
||||
.left_avatar(user.avatar_uri.clone())
|
||||
.start_slot(Avatar::new(user.avatar_uri.clone()))
|
||||
}
|
||||
|
||||
fn render_contact_placeholder(
|
||||
@ -2541,6 +2550,8 @@ impl CollabPanel {
|
||||
div()
|
||||
.id(channel_id as usize)
|
||||
.group("")
|
||||
.flex()
|
||||
.w_full()
|
||||
.on_drag({
|
||||
let channel = channel.clone();
|
||||
move |cx| {
|
||||
@ -2566,67 +2577,10 @@ impl CollabPanel {
|
||||
)
|
||||
.child(
|
||||
ListItem::new(channel_id as usize)
|
||||
.indent_level(depth)
|
||||
// Offset the indent depth by one to give us room to show the disclosure.
|
||||
.indent_level(depth + 1)
|
||||
.indent_step_size(cx.rem_size() * 14.0 / 16.0) // @todo()! @nate this is to step over the disclosure toggle
|
||||
.left_icon(if is_public { Icon::Public } else { Icon::Hash })
|
||||
.selected(is_selected || is_active)
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
.justify_between()
|
||||
.child(
|
||||
h_stack()
|
||||
.id(channel_id as usize)
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.child(
|
||||
div()
|
||||
.id("channel_chat")
|
||||
.when(!has_messages_notification, |el| el.invisible())
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new(
|
||||
"channel_chat",
|
||||
Icon::MessageBubbles,
|
||||
)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel chat", cx)
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("channel_notes")
|
||||
.when(!has_notes_notification, |el| el.invisible())
|
||||
.group_hover("", |style| style.visible())
|
||||
.child(
|
||||
IconButton::new("channel_notes", Icon::File)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| {
|
||||
Tooltip::text("Open channel notes", cx)
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toggle(disclosed)
|
||||
.on_toggle(
|
||||
cx.listener(move |this, _, cx| {
|
||||
@ -2646,7 +2600,49 @@ impl CollabPanel {
|
||||
move |this, event: &MouseDownEvent, cx| {
|
||||
this.deploy_channel_context_menu(event.position, channel_id, ix, cx)
|
||||
},
|
||||
)),
|
||||
))
|
||||
.start_slot(
|
||||
IconElement::new(if is_public { Icon::Public } else { Icon::Hash })
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.id(channel_id as usize)
|
||||
.child(Label::new(channel.name.clone()))
|
||||
.children(face_pile.map(|face_pile| face_pile.render(cx))),
|
||||
)
|
||||
.end_slot(
|
||||
h_stack()
|
||||
.child(
|
||||
IconButton::new("channel_chat", Icon::MessageBubbles)
|
||||
.icon_color(if has_messages_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.when(!has_messages_notification, |this| {
|
||||
this.visible_on_hover("")
|
||||
})
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.join_channel_chat(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel chat", cx)),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("channel_notes", Icon::File)
|
||||
.icon_color(if has_notes_notification {
|
||||
Color::Default
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
.when(!has_notes_notification, |this| this.visible_on_hover(""))
|
||||
.on_click(cx.listener(move |this, _, cx| {
|
||||
this.open_channel_notes(channel_id, cx)
|
||||
}))
|
||||
.tooltip(|cx| Tooltip::text("Open channel notes", cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
.tooltip(|cx| Tooltip::text("Join channel", cx))
|
||||
|
||||
@ -2962,7 +2958,11 @@ impl CollabPanel {
|
||||
let item = ListItem::new("channel-editor")
|
||||
.inset(false)
|
||||
.indent_level(depth)
|
||||
.left_icon(Icon::Hash);
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Hash)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
|
||||
if let Some(pending_name) = self
|
||||
.channel_editing_state
|
||||
|
@ -9763,19 +9763,15 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend
|
||||
.px_1p5()
|
||||
.child(HighlightedLabel::new(line.clone(), highlights.clone()))
|
||||
.child(
|
||||
div()
|
||||
.border()
|
||||
.border_color(gpui::red())
|
||||
.invisible()
|
||||
.group_hover(group_id, |style| style.visible())
|
||||
.child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
div().border().border_color(gpui::red()).child(
|
||||
IconButton::new(copy_id.clone(), Icon::Copy)
|
||||
.icon_color(Color::Muted)
|
||||
.size(ButtonSize::Compact)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.visible_on_hover(group_id)
|
||||
.on_click(cx.listener(move |_, _, cx| write_to_clipboard))
|
||||
.tooltip(|cx| Tooltip::text("Copy diagnostic message", cx)),
|
||||
),
|
||||
)
|
||||
}))
|
||||
.into_any_element()
|
||||
|
@ -53,7 +53,7 @@ pub struct FeedbackModal {
|
||||
email_address_editor: View<Editor>,
|
||||
awaiting_submission: bool,
|
||||
user_submitted: bool,
|
||||
discarded: bool,
|
||||
user_discarded: bool,
|
||||
character_count: i32,
|
||||
}
|
||||
|
||||
@ -71,7 +71,7 @@ impl ModalView for FeedbackModal {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.discarded {
|
||||
if self.user_discarded {
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -85,7 +85,7 @@ impl ModalView for FeedbackModal {
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
if answer.await.ok() == Some(0) {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.discarded = true;
|
||||
this.user_discarded = true;
|
||||
cx.emit(DismissEvent)
|
||||
})
|
||||
.log_err();
|
||||
@ -184,7 +184,7 @@ impl FeedbackModal {
|
||||
email_address_editor,
|
||||
awaiting_submission: false,
|
||||
user_submitted: false,
|
||||
discarded: false,
|
||||
user_discarded: false,
|
||||
character_count: 0,
|
||||
}
|
||||
}
|
||||
@ -309,7 +309,6 @@ impl FeedbackModal {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// TODO: Escape button calls dismiss
|
||||
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(DismissEvent)
|
||||
}
|
||||
|
@ -893,7 +893,7 @@ impl Interactivity {
|
||||
if style
|
||||
.background
|
||||
.as_ref()
|
||||
.is_some_and(|fill| fill.color().is_some_and(|color| !color.is_transparent()))
|
||||
.is_some_and(|fill| fill.color().is_some())
|
||||
{
|
||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| cx.add_opaque_layer(bounds))
|
||||
}
|
||||
@ -1273,78 +1273,84 @@ impl Interactivity {
|
||||
let mut style = Style::default();
|
||||
style.refine(&self.base_style);
|
||||
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
if let Some(in_focus_style) = self.in_focus_style.as_ref() {
|
||||
if focus_handle.within_focused(cx) {
|
||||
style.refine(in_focus_style);
|
||||
cx.with_z_index(style.z_index.unwrap_or(0), |cx| {
|
||||
if let Some(focus_handle) = self.tracked_focus_handle.as_ref() {
|
||||
if let Some(in_focus_style) = self.in_focus_style.as_ref() {
|
||||
if focus_handle.within_focused(cx) {
|
||||
style.refine(in_focus_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(focus_style) = self.focus_style.as_ref() {
|
||||
if focus_handle.is_focused(cx) {
|
||||
style.refine(focus_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bounds) = bounds {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
||||
if group_bounds.contains(&mouse_position)
|
||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||
{
|
||||
style.refine(&group_hover.style);
|
||||
if let Some(focus_style) = self.focus_style.as_ref() {
|
||||
if focus_handle.is_focused(cx) {
|
||||
style.refine(focus_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(hover_style) = self.hover_style.as_ref() {
|
||||
if bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
.contains(&mouse_position)
|
||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||
{
|
||||
style.refine(hover_style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(drag) = cx.active_drag.take() {
|
||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
||||
if *state_type == drag.view.entity_type()
|
||||
&& group_bounds.contains(&mouse_position)
|
||||
if let Some(bounds) = bounds {
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(group_hover) = self.group_hover_style.as_ref() {
|
||||
if let Some(group_bounds) = GroupBounds::get(&group_hover.group, cx) {
|
||||
if group_bounds.contains(&mouse_position)
|
||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||
{
|
||||
style.refine(&group_drag_style.style);
|
||||
style.refine(&group_hover.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (state_type, drag_over_style) in &self.drag_over_styles {
|
||||
if *state_type == drag.view.entity_type()
|
||||
&& bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
.contains(&mouse_position)
|
||||
if let Some(hover_style) = self.hover_style.as_ref() {
|
||||
if bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
.contains(&mouse_position)
|
||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||
{
|
||||
style.refine(drag_over_style);
|
||||
style.refine(hover_style);
|
||||
}
|
||||
}
|
||||
|
||||
cx.active_drag = Some(drag);
|
||||
}
|
||||
}
|
||||
if let Some(drag) = cx.active_drag.take() {
|
||||
for (state_type, group_drag_style) in &self.group_drag_over_styles {
|
||||
if let Some(group_bounds) = GroupBounds::get(&group_drag_style.group, cx) {
|
||||
if *state_type == drag.view.entity_type()
|
||||
&& group_bounds.contains(&mouse_position)
|
||||
{
|
||||
style.refine(&group_drag_style.style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clicked_state = element_state.clicked_state.borrow();
|
||||
if clicked_state.group {
|
||||
if let Some(group) = self.group_active_style.as_ref() {
|
||||
style.refine(&group.style)
|
||||
}
|
||||
}
|
||||
for (state_type, drag_over_style) in &self.drag_over_styles {
|
||||
if *state_type == drag.view.entity_type()
|
||||
&& bounds
|
||||
.intersect(&cx.content_mask().bounds)
|
||||
.contains(&mouse_position)
|
||||
&& cx.was_top_layer_under_active_drag(
|
||||
&mouse_position,
|
||||
cx.stacking_order(),
|
||||
)
|
||||
{
|
||||
style.refine(drag_over_style);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_style) = self.active_style.as_ref() {
|
||||
if clicked_state.element {
|
||||
style.refine(active_style)
|
||||
cx.active_drag = Some(drag);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let clicked_state = element_state.clicked_state.borrow();
|
||||
if clicked_state.group {
|
||||
if let Some(group) = self.group_active_style.as_ref() {
|
||||
style.refine(&group.style)
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(active_style) = self.active_style.as_ref() {
|
||||
if clicked_state.element {
|
||||
style.refine(active_style)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
style
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ pub type LayerId = u32;
|
||||
pub type DrawOrder = u32;
|
||||
|
||||
pub(crate) struct SceneBuilder {
|
||||
last_order: Option<(StackingOrder, LayerId)>,
|
||||
layers_by_order: BTreeMap<StackingOrder, LayerId>,
|
||||
splitter: BspSplitter<(PrimitiveKind, usize)>,
|
||||
shadows: Vec<Shadow>,
|
||||
@ -31,6 +32,7 @@ pub(crate) struct SceneBuilder {
|
||||
impl Default for SceneBuilder {
|
||||
fn default() -> Self {
|
||||
SceneBuilder {
|
||||
last_order: None,
|
||||
layers_by_order: BTreeMap::new(),
|
||||
splitter: BspSplitter::new(),
|
||||
shadows: Vec::new(),
|
||||
@ -52,6 +54,7 @@ impl SceneBuilder {
|
||||
layer_z_values[*layer_id as usize] = ix as f32 / self.layers_by_order.len() as f32;
|
||||
}
|
||||
self.layers_by_order.clear();
|
||||
self.last_order = None;
|
||||
|
||||
// Add all primitives to the BSP splitter to determine draw order
|
||||
self.splitter.reset();
|
||||
@ -156,14 +159,7 @@ impl SceneBuilder {
|
||||
return;
|
||||
}
|
||||
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
|
||||
let layer_id = self.layer_id_for_order(order);
|
||||
match primitive {
|
||||
Primitive::Shadow(mut shadow) => {
|
||||
shadow.order = layer_id;
|
||||
@ -196,6 +192,24 @@ impl SceneBuilder {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn layer_id_for_order(&mut self, order: &StackingOrder) -> u32 {
|
||||
if let Some((last_order, last_layer_id)) = self.last_order.as_ref() {
|
||||
if last_order == order {
|
||||
return *last_layer_id;
|
||||
}
|
||||
};
|
||||
|
||||
let layer_id = if let Some(layer_id) = self.layers_by_order.get(order) {
|
||||
*layer_id
|
||||
} else {
|
||||
let next_id = self.layers_by_order.len() as LayerId;
|
||||
self.layers_by_order.insert(order.clone(), next_id);
|
||||
next_id
|
||||
};
|
||||
self.last_order = Some((order.clone(), layer_id));
|
||||
layer_id
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Scene {
|
||||
|
@ -16,6 +16,7 @@ menu = { package = "menu2", path = "../menu2" }
|
||||
settings = { package = "settings2", path = "../settings2" }
|
||||
util = { path = "../util" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
workspace = { package = "workspace2", path = "../workspace2"}
|
||||
|
||||
parking_lot.workspace = true
|
||||
|
||||
|
@ -1,11 +1,12 @@
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
div, prelude::*, rems, uniform_list, AnyElement, AppContext, DismissEvent, Div, EventEmitter,
|
||||
FocusHandle, FocusableView, MouseButton, MouseDownEvent, Render, Task, UniformListScrollHandle,
|
||||
View, ViewContext, WindowContext,
|
||||
FocusHandle, FocusableView, Length, MouseButton, MouseDownEvent, Render, Task,
|
||||
UniformListScrollHandle, View, ViewContext, WindowContext,
|
||||
};
|
||||
use std::{cmp, sync::Arc};
|
||||
use ui::{prelude::*, v_stack, Color, Divider, Label};
|
||||
use workspace::ModalView;
|
||||
|
||||
pub struct Picker<D: PickerDelegate> {
|
||||
pub delegate: D,
|
||||
@ -13,6 +14,7 @@ pub struct Picker<D: PickerDelegate> {
|
||||
editor: View<Editor>,
|
||||
pending_update_matches: Option<Task<()>>,
|
||||
confirm_on_update: Option<bool>,
|
||||
width: Option<Length>,
|
||||
}
|
||||
|
||||
pub trait PickerDelegate: Sized + 'static {
|
||||
@ -55,11 +57,17 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
scroll_handle: UniformListScrollHandle::new(),
|
||||
pending_update_matches: None,
|
||||
confirm_on_update: None,
|
||||
width: None,
|
||||
};
|
||||
this.update_matches("".to_string(), cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn width(mut self, width: impl Into<gpui::Length>) -> Self {
|
||||
self.width = Some(width.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn focus(&self, cx: &mut WindowContext) {
|
||||
self.editor.update(cx, |editor, cx| editor.focus(cx));
|
||||
}
|
||||
@ -197,6 +205,7 @@ impl<D: PickerDelegate> Picker<D> {
|
||||
}
|
||||
|
||||
impl<D: PickerDelegate> EventEmitter<DismissEvent> for Picker<D> {}
|
||||
impl<D: PickerDelegate> ModalView for Picker<D> {}
|
||||
|
||||
impl<D: PickerDelegate> Render for Picker<D> {
|
||||
type Element = Div;
|
||||
@ -221,6 +230,9 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
div()
|
||||
.key_context("picker")
|
||||
.size_full()
|
||||
.when_some(self.width, |el, width| {
|
||||
el.w(width)
|
||||
})
|
||||
.overflow_hidden()
|
||||
.elevation_3(cx)
|
||||
.on_action(cx.listener(Self::select_next))
|
||||
@ -271,7 +283,6 @@ impl<D: PickerDelegate> Render for Picker<D> {
|
||||
},
|
||||
)
|
||||
.track_scroll(self.scroll_handle.clone())
|
||||
.p_1()
|
||||
)
|
||||
.max_h_72()
|
||||
.overflow_hidden(),
|
||||
|
37
crates/project_symbols2/Cargo.toml
Normal file
37
crates/project_symbols2/Cargo.toml
Normal file
@ -0,0 +1,37 @@
|
||||
[package]
|
||||
name = "project_symbols2"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
publish = false
|
||||
|
||||
[lib]
|
||||
path = "src/project_symbols.rs"
|
||||
doctest = false
|
||||
|
||||
[dependencies]
|
||||
editor = { package = "editor2", path = "../editor2" }
|
||||
fuzzy = {package = "fuzzy2", path = "../fuzzy2" }
|
||||
gpui = {package = "gpui2", path = "../gpui2" }
|
||||
picker = {package = "picker2", path = "../picker2" }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
text = {package = "text2", path = "../text2" }
|
||||
settings = {package = "settings2", path = "../settings2" }
|
||||
workspace = {package = "workspace2", path = "../workspace2" }
|
||||
theme = { package = "theme2", path = "../theme2" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow.workspace = true
|
||||
ordered-float.workspace = true
|
||||
postage.workspace = true
|
||||
smol.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
futures.workspace = true
|
||||
editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
|
||||
settings = { package = "settings2", path = "../settings2", features = ["test-support"] }
|
||||
gpui = { package = "gpui2", path = "../gpui2", features = ["test-support"] }
|
||||
language = { package = "language2", path = "../language2", features = ["test-support"] }
|
||||
lsp = { package = "lsp2", path = "../lsp2", features = ["test-support"] }
|
||||
project = { package = "project2", path = "../project2", features = ["test-support"] }
|
||||
theme = { package = "theme2", path = "../theme2", features = ["test-support"] }
|
||||
workspace = { package = "workspace2", path = "../workspace2", features = ["test-support"] }
|
411
crates/project_symbols2/src/project_symbols.rs
Normal file
411
crates/project_symbols2/src/project_symbols.rs
Normal file
@ -0,0 +1,411 @@
|
||||
use editor::{scroll::autoscroll::Autoscroll, styled_runs_for_code_label, Bias, Editor};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions, rems, AppContext, DismissEvent, FontWeight, Model, ParentElement, StyledText, Task,
|
||||
View, ViewContext, WeakView,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{Project, Symbol};
|
||||
use std::{borrow::Cow, cmp::Reverse, sync::Arc};
|
||||
use theme::ActiveTheme;
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
ui::{v_stack, Color, Label, LabelCommon, LabelLike, ListItem, Selectable},
|
||||
Workspace,
|
||||
};
|
||||
|
||||
actions!(project_symbols, [Toggle]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.observe_new_views(
|
||||
|workspace: &mut Workspace, _: &mut ViewContext<Workspace>| {
|
||||
workspace.register_action(|workspace, _: &Toggle, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let handle = cx.view().downgrade();
|
||||
workspace.toggle_modal(cx, move |cx| {
|
||||
let delegate = ProjectSymbolsDelegate::new(handle, project);
|
||||
Picker::new(delegate, cx).width(rems(34.))
|
||||
})
|
||||
});
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
}
|
||||
|
||||
pub type ProjectSymbols = View<Picker<ProjectSymbolsDelegate>>;
|
||||
|
||||
pub struct ProjectSymbolsDelegate {
|
||||
workspace: WeakView<Workspace>,
|
||||
project: Model<Project>,
|
||||
selected_match_index: usize,
|
||||
symbols: Vec<Symbol>,
|
||||
visible_match_candidates: Vec<StringMatchCandidate>,
|
||||
external_match_candidates: Vec<StringMatchCandidate>,
|
||||
show_worktree_root_name: bool,
|
||||
matches: Vec<StringMatch>,
|
||||
}
|
||||
|
||||
impl ProjectSymbolsDelegate {
|
||||
fn new(workspace: WeakView<Workspace>, project: Model<Project>) -> Self {
|
||||
Self {
|
||||
workspace,
|
||||
project,
|
||||
selected_match_index: 0,
|
||||
symbols: Default::default(),
|
||||
visible_match_candidates: Default::default(),
|
||||
external_match_candidates: Default::default(),
|
||||
matches: Default::default(),
|
||||
show_worktree_root_name: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn filter(&mut self, query: &str, cx: &mut ViewContext<Picker<Self>>) {
|
||||
const MAX_MATCHES: usize = 100;
|
||||
let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&self.visible_match_candidates,
|
||||
query,
|
||||
false,
|
||||
MAX_MATCHES,
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
|
||||
&self.external_match_candidates,
|
||||
query,
|
||||
false,
|
||||
MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
|
||||
&Default::default(),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
let sort_key_for_match = |mat: &StringMatch| {
|
||||
let symbol = &self.symbols[mat.candidate_id];
|
||||
(
|
||||
Reverse(OrderedFloat(mat.score)),
|
||||
&symbol.label.text[symbol.label.filter_range.clone()],
|
||||
)
|
||||
};
|
||||
|
||||
visible_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
external_matches.sort_unstable_by_key(sort_key_for_match);
|
||||
let mut matches = visible_matches;
|
||||
matches.append(&mut external_matches);
|
||||
|
||||
for mat in &mut matches {
|
||||
let symbol = &self.symbols[mat.candidate_id];
|
||||
let filter_start = symbol.label.filter_range.start;
|
||||
for position in &mut mat.positions {
|
||||
*position += filter_start;
|
||||
}
|
||||
}
|
||||
|
||||
self.matches = matches;
|
||||
self.set_selected_index(0, cx);
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ProjectSymbolsDelegate {
|
||||
type ListItem = ListItem;
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search project symbols...".into()
|
||||
}
|
||||
|
||||
fn confirm(&mut self, secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some(symbol) = self
|
||||
.matches
|
||||
.get(self.selected_match_index)
|
||||
.map(|mat| self.symbols[mat.candidate_id].clone())
|
||||
{
|
||||
let buffer = self.project.update(cx, |project, cx| {
|
||||
project.open_buffer_for_symbol(&symbol, cx)
|
||||
});
|
||||
let symbol = symbol.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
cx.spawn(|_, mut cx| async move {
|
||||
let buffer = buffer.await?;
|
||||
workspace.update(&mut cx, |workspace, cx| {
|
||||
let position = buffer
|
||||
.read(cx)
|
||||
.clip_point_utf16(symbol.range.start, Bias::Left);
|
||||
|
||||
let editor = if secondary {
|
||||
workspace.split_project_item::<Editor>(buffer, cx)
|
||||
} else {
|
||||
workspace.open_project_item::<Editor>(buffer, cx)
|
||||
};
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Some(Autoscroll::center()), cx, |s| {
|
||||
s.select_ranges([position..position])
|
||||
});
|
||||
});
|
||||
})?;
|
||||
Ok::<_, anyhow::Error>(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_match_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_match_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
self.filter(&query, cx);
|
||||
self.show_worktree_root_name = self.project.read(cx).visible_worktrees(cx).count() > 1;
|
||||
let symbols = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.symbols(&query, cx));
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let symbols = symbols.await.log_err();
|
||||
if let Some(symbols) = symbols {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
let delegate = &mut this.delegate;
|
||||
let project = delegate.project.read(cx);
|
||||
let (visible_match_candidates, external_match_candidates) = symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| {
|
||||
StringMatchCandidate::new(
|
||||
id,
|
||||
symbol.label.text[symbol.label.filter_range.clone()].to_string(),
|
||||
)
|
||||
})
|
||||
.partition(|candidate| {
|
||||
project
|
||||
.entry_for_path(&symbols[candidate.id].path, cx)
|
||||
.map_or(false, |e| !e.is_ignored)
|
||||
});
|
||||
|
||||
delegate.visible_match_candidates = visible_match_candidates;
|
||||
delegate.external_match_candidates = external_match_candidates;
|
||||
delegate.symbols = symbols;
|
||||
delegate.filter(&query, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
cx: &mut ViewContext<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let string_match = &self.matches[ix];
|
||||
let symbol = &self.symbols[string_match.candidate_id];
|
||||
let syntax_runs = styled_runs_for_code_label(&symbol.label, cx.theme().syntax());
|
||||
|
||||
let mut path = symbol.path.path.to_string_lossy();
|
||||
if self.show_worktree_root_name {
|
||||
let project = self.project.read(cx);
|
||||
if let Some(worktree) = project.worktree_for_id(symbol.path.worktree_id, cx) {
|
||||
path = Cow::Owned(format!(
|
||||
"{}{}{}",
|
||||
worktree.read(cx).root_name(),
|
||||
std::path::MAIN_SEPARATOR,
|
||||
path.as_ref()
|
||||
));
|
||||
}
|
||||
}
|
||||
let label = symbol.label.text.clone();
|
||||
let path = path.to_string().clone();
|
||||
|
||||
let highlights = gpui::combine_highlights(
|
||||
string_match
|
||||
.positions
|
||||
.iter()
|
||||
.map(|pos| (*pos..pos + 1, FontWeight::BOLD.into())),
|
||||
syntax_runs.map(|(range, mut highlight)| {
|
||||
// Ignore font weight for syntax highlighting, as we'll use it
|
||||
// for fuzzy matches.
|
||||
highlight.font_weight = None;
|
||||
(range, highlight)
|
||||
}),
|
||||
);
|
||||
|
||||
Some(
|
||||
ListItem::new(ix).inset(true).selected(selected).child(
|
||||
// todo!() combine_syntax_and_fuzzy_match_highlights()
|
||||
v_stack()
|
||||
.child(
|
||||
LabelLike::new().child(
|
||||
StyledText::new(label)
|
||||
.with_highlights(&cx.text_style().clone(), highlights),
|
||||
),
|
||||
)
|
||||
.child(Label::new(path).color(Color::Muted)),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::StreamExt;
|
||||
use gpui::{serde_json::json, TestAppContext, VisualContext};
|
||||
use language::{FakeLspAdapter, Language, LanguageConfig};
|
||||
use project::FakeFs;
|
||||
use settings::SettingsStore;
|
||||
use std::{path::Path, sync::Arc};
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_project_symbols(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
let mut language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
path_suffixes: vec!["rs".to_string()],
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
);
|
||||
let mut fake_servers = language
|
||||
.set_fake_lsp_adapter(Arc::<FakeLspAdapter>::default())
|
||||
.await;
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree("/dir", json!({ "test.rs": "" })).await;
|
||||
|
||||
let project = Project::test(fs.clone(), ["/dir".as_ref()], cx).await;
|
||||
project.update(cx, |project, _| project.languages().add(Arc::new(language)));
|
||||
|
||||
let _buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer("/dir/test.rs", cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// Set up fake language server to return fuzzy matches against
|
||||
// a fixed set of symbol names.
|
||||
let fake_symbols = [
|
||||
symbol("one", "/external"),
|
||||
symbol("ton", "/dir/test.rs"),
|
||||
symbol("uno", "/dir/test.rs"),
|
||||
];
|
||||
let fake_server = fake_servers.next().await.unwrap();
|
||||
fake_server.handle_request::<lsp::WorkspaceSymbolRequest, _, _>(
|
||||
move |params: lsp::WorkspaceSymbolParams, cx| {
|
||||
let executor = cx.background_executor().clone();
|
||||
let fake_symbols = fake_symbols.clone();
|
||||
async move {
|
||||
let candidates = fake_symbols
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, symbol)| StringMatchCandidate::new(id, symbol.name.clone()))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if params.query.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
¶ms.query,
|
||||
true,
|
||||
100,
|
||||
&Default::default(),
|
||||
executor.clone(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
Ok(Some(lsp::WorkspaceSymbolResponse::Flat(
|
||||
matches
|
||||
.into_iter()
|
||||
.map(|mat| fake_symbols[mat.candidate_id].clone())
|
||||
.collect(),
|
||||
)))
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
let (workspace, cx) = cx.add_window_view(|cx| Workspace::test_new(project.clone(), cx));
|
||||
|
||||
// Create the project symbols view.
|
||||
let symbols = cx.build_view(|cx| {
|
||||
Picker::new(
|
||||
ProjectSymbolsDelegate::new(workspace.downgrade(), project.clone()),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
// Spawn multiples updates before the first update completes,
|
||||
// such that in the end, there are no matches. Testing for regression:
|
||||
// https://github.com/zed-industries/zed/issues/861
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("o".to_string(), cx);
|
||||
p.update_matches("on".to_string(), cx);
|
||||
p.update_matches("onex".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||
});
|
||||
|
||||
// Spawn more updates such that in the end, there are matches.
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("one".to_string(), cx);
|
||||
p.update_matches("on".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
let delegate = &symbols.delegate;
|
||||
assert_eq!(delegate.matches.len(), 2);
|
||||
assert_eq!(delegate.matches[0].string, "ton");
|
||||
assert_eq!(delegate.matches[1].string, "one");
|
||||
});
|
||||
|
||||
// Spawn more updates such that in the end, there are again no matches.
|
||||
symbols.update(cx, |p, cx| {
|
||||
p.update_matches("o".to_string(), cx);
|
||||
p.update_matches("".to_string(), cx);
|
||||
});
|
||||
|
||||
cx.run_until_parked();
|
||||
symbols.update(cx, |symbols, _| {
|
||||
assert_eq!(symbols.delegate.matches.len(), 0);
|
||||
});
|
||||
}
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
cx.update(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
theme::init(theme::LoadThemes::JustBase, cx);
|
||||
language::init(cx);
|
||||
Project::init_settings(cx);
|
||||
workspace::init_settings(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn symbol(name: &str, path: impl AsRef<Path>) -> lsp::SymbolInformation {
|
||||
#[allow(deprecated)]
|
||||
lsp::SymbolInformation {
|
||||
name: name.to_string(),
|
||||
kind: lsp::SymbolKind::FUNCTION,
|
||||
tags: None,
|
||||
deprecated: None,
|
||||
container_name: None,
|
||||
location: lsp::Location::new(
|
||||
lsp::Url::from_file_path(path.as_ref()).unwrap(),
|
||||
lsp::Range::new(lsp::Position::new(0, 0), lsp::Position::new(0, 0)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
@ -2,7 +2,6 @@ use gpui::{relative, DefiniteLength};
|
||||
use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::h_stack;
|
||||
use crate::prelude::*;
|
||||
|
||||
pub trait ButtonCommon: Clickable + Disableable {
|
||||
@ -250,6 +249,7 @@ impl ButtonSize {
|
||||
/// This is also used to build the prebuilt buttons.
|
||||
#[derive(IntoElement)]
|
||||
pub struct ButtonLike {
|
||||
base: Div,
|
||||
id: ElementId,
|
||||
pub(super) style: ButtonStyle,
|
||||
pub(super) disabled: bool,
|
||||
@ -264,6 +264,7 @@ pub struct ButtonLike {
|
||||
impl ButtonLike {
|
||||
pub fn new(id: impl Into<ElementId>) -> Self {
|
||||
Self {
|
||||
base: div(),
|
||||
id: id.into(),
|
||||
style: ButtonStyle::default(),
|
||||
disabled: false,
|
||||
@ -331,6 +332,13 @@ impl ButtonCommon for ButtonLike {
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for ButtonLike {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for ButtonLike {
|
||||
fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
|
||||
&mut self.children
|
||||
@ -341,7 +349,8 @@ impl RenderOnce for ButtonLike {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack()
|
||||
self.base
|
||||
.h_flex()
|
||||
.id(self.id.clone())
|
||||
.group("")
|
||||
.flex_none()
|
||||
|
@ -98,6 +98,13 @@ impl ButtonCommon for IconButton {
|
||||
}
|
||||
}
|
||||
|
||||
impl VisibleOnHover for IconButton {
|
||||
fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.base = self.base.visible_on_hover(group_name);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
type Rendered = ButtonLike;
|
||||
|
||||
|
@ -255,6 +255,9 @@ impl Render for ContextMenu {
|
||||
};
|
||||
|
||||
ListItem::new(label.clone())
|
||||
.inset(true)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| handler(cx))
|
||||
.child(
|
||||
h_stack()
|
||||
.w_full()
|
||||
@ -265,8 +268,6 @@ impl Render for ContextMenu {
|
||||
.map(|binding| div().ml_1().child(binding))
|
||||
})),
|
||||
)
|
||||
.selected(Some(ix) == self.selected_index)
|
||||
.on_click(move |_, cx| handler(cx))
|
||||
.into_any_element()
|
||||
}
|
||||
},
|
||||
|
@ -1,6 +1,7 @@
|
||||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||
use gpui::ClickEvent;
|
||||
|
||||
use crate::{prelude::*, Color, Icon, IconButton, IconSize};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Disclosure {
|
||||
is_open: bool,
|
||||
|
@ -1,12 +1,18 @@
|
||||
use crate::{h_stack, prelude::*, Disclosure, Icon, IconElement, IconSize, Label};
|
||||
use crate::{h_stack, prelude::*, Disclosure, Label};
|
||||
use gpui::{AnyElement, ClickEvent, Div};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListHeader {
|
||||
/// The label of the header.
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
meta: SmallVec<[AnyElement; 2]>,
|
||||
/// A slot for content that appears before the label, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the label, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the label
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
on_toggle: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
inset: bool,
|
||||
@ -17,8 +23,9 @@ impl ListHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
meta: SmallVec::new(),
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
inset: false,
|
||||
toggle: None,
|
||||
on_toggle: None,
|
||||
@ -39,13 +46,23 @@ impl ListHeader {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: impl Into<Option<Icon>>) -> Self {
|
||||
self.left_icon = left_icon.into();
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn meta(mut self, meta: impl IntoElement) -> Self {
|
||||
self.meta.push(meta.into_any_element());
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn inset(mut self, inset: bool) -> Self {
|
||||
self.inset = inset;
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -61,9 +78,9 @@ impl RenderOnce for ListHeader {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
h_stack().w_full().relative().child(
|
||||
h_stack().w_full().relative().group("list_header").child(
|
||||
div()
|
||||
.h_5()
|
||||
.h_7()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
@ -77,24 +94,29 @@ impl RenderOnce for ListHeader {
|
||||
.child(
|
||||
h_stack()
|
||||
.gap_1()
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
}))
|
||||
.children(self.start_slot)
|
||||
.child(Label::new(self.label.clone()).color(Color::Muted)),
|
||||
)
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
),
|
||||
)
|
||||
.child(h_stack().gap_2().items_center().children(self.meta)),
|
||||
.child(h_stack().children(self.end_slot))
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_0()
|
||||
.visible_on_hover("list_header")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,19 +1,24 @@
|
||||
use crate::{prelude::*, Avatar, Disclosure, Icon, IconElement, IconSize};
|
||||
use gpui::{
|
||||
px, AnyElement, AnyView, ClickEvent, Div, ImageSource, MouseButton, MouseDownEvent, Pixels,
|
||||
Stateful,
|
||||
px, AnyElement, AnyView, ClickEvent, Div, MouseButton, MouseDownEvent, Pixels, Stateful,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use crate::{prelude::*, Disclosure};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListItem {
|
||||
id: ElementId,
|
||||
selected: bool,
|
||||
// TODO: Reintroduce this
|
||||
// disclosure_control_style: DisclosureControlVisibility,
|
||||
indent_level: usize,
|
||||
indent_step_size: Pixels,
|
||||
left_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears before the children, like an icon or avatar.
|
||||
start_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears after the children, usually on the other side of the header.
|
||||
/// This might be a button, a disclosure arrow, a face pile, etc.
|
||||
end_slot: Option<AnyElement>,
|
||||
/// A slot for content that appears on hover after the children
|
||||
/// It will obscure the `end_slot` when visible.
|
||||
end_hover_slot: Option<AnyElement>,
|
||||
toggle: Option<bool>,
|
||||
inset: bool,
|
||||
on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
|
||||
@ -30,7 +35,9 @@ impl ListItem {
|
||||
selected: false,
|
||||
indent_level: 0,
|
||||
indent_step_size: px(12.),
|
||||
left_slot: None,
|
||||
start_slot: None,
|
||||
end_slot: None,
|
||||
end_hover_slot: None,
|
||||
toggle: None,
|
||||
inset: false,
|
||||
on_click: None,
|
||||
@ -87,23 +94,18 @@ impl ListItem {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_child(mut self, left_content: impl IntoElement) -> Self {
|
||||
self.left_slot = Some(left_content.into_any_element());
|
||||
pub fn start_slot<E: IntoElement>(mut self, start_slot: impl Into<Option<E>>) -> Self {
|
||||
self.start_slot = start_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Icon) -> Self {
|
||||
self.left_slot = Some(
|
||||
IconElement::new(left_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.into_any_element(),
|
||||
);
|
||||
pub fn end_slot<E: IntoElement>(mut self, end_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_slot = end_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
|
||||
self.left_slot = Some(Avatar::new(left_avatar).into_any_element());
|
||||
pub fn end_hover_slot<E: IntoElement>(mut self, end_hover_slot: impl Into<Option<E>>) -> Self {
|
||||
self.end_hover_slot = end_hover_slot.into().map(IntoElement::into_any_element);
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -125,49 +127,102 @@ impl RenderOnce for ListItem {
|
||||
type Rendered = Stateful<Div>;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div()
|
||||
h_stack()
|
||||
.id(self.id)
|
||||
.w_full()
|
||||
.relative()
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.when(self.inset, |this| this.rounded_md())
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
// When an item is inset draw the indent spacing outside of the item
|
||||
.when(self.inset, |this| {
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.px_1()
|
||||
})
|
||||
.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 {
|
||||
(on_click)(event, cx)
|
||||
}
|
||||
})
|
||||
.when(!self.inset, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
.child(
|
||||
div()
|
||||
.when(self.inset, |this| this.px_2())
|
||||
.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
h_stack()
|
||||
.id("inner_list_item")
|
||||
.w_full()
|
||||
.relative()
|
||||
.children(
|
||||
self.toggle
|
||||
.map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)),
|
||||
.gap_1()
|
||||
.px_2()
|
||||
.group("list_item")
|
||||
.when(self.inset, |this| {
|
||||
this
|
||||
// TODO: Add focus state
|
||||
// .when(self.state == InteractionState::Focused, |this| {
|
||||
// this.border()
|
||||
// .border_color(cx.theme().colors().border_focused)
|
||||
// })
|
||||
.hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
|
||||
.active(|style| style.bg(cx.theme().colors().ghost_element_active))
|
||||
.when(self.selected, |this| {
|
||||
this.bg(cx.theme().colors().ghost_element_selected)
|
||||
})
|
||||
})
|
||||
.when_some(self.on_click, |this, on_click| {
|
||||
this.cursor_pointer().on_click(on_click)
|
||||
})
|
||||
.when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
|
||||
this.on_mouse_down(MouseButton::Right, move |event, cx| {
|
||||
(on_mouse_down)(event, cx)
|
||||
})
|
||||
})
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
.map(|this| {
|
||||
if self.inset {
|
||||
this.rounded_md()
|
||||
} else {
|
||||
// When an item is not inset draw the indent spacing inside of the item
|
||||
this.ml(self.indent_level as f32 * self.indent_step_size)
|
||||
}
|
||||
})
|
||||
.children(self.toggle.map(|is_open| {
|
||||
div()
|
||||
.flex()
|
||||
.absolute()
|
||||
.left(rems(-1.))
|
||||
.visible_on_hover("")
|
||||
.child(Disclosure::new(is_open).on_toggle(self.on_toggle))
|
||||
}))
|
||||
.child(
|
||||
h_stack()
|
||||
.flex_1()
|
||||
.gap_1()
|
||||
.children(self.start_slot)
|
||||
.children(self.children),
|
||||
)
|
||||
.children(self.left_slot)
|
||||
.children(self.children),
|
||||
.when_some(self.end_slot, |this, end_slot| {
|
||||
this.justify_between().child(
|
||||
h_stack()
|
||||
.when(self.end_hover_slot.is_some(), |this| {
|
||||
this.visible()
|
||||
.group_hover("list_item", |this| this.invisible())
|
||||
})
|
||||
.child(end_slot),
|
||||
)
|
||||
})
|
||||
.when_some(self.end_hover_slot, |this, end_hover_slot| {
|
||||
this.child(
|
||||
h_stack()
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_2()
|
||||
.top_0()
|
||||
.visible_on_hover("list_item")
|
||||
.child(end_hover_slot),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -9,6 +9,10 @@ impl RenderOnce for ListSeparator {
|
||||
type Rendered = Div;
|
||||
|
||||
fn render(self, cx: &mut WindowContext) -> Self::Rendered {
|
||||
div().h_px().w_full().bg(cx.theme().colors().border_variant)
|
||||
div()
|
||||
.h_px()
|
||||
.w_full()
|
||||
.my_1()
|
||||
.bg(cx.theme().colors().border_variant)
|
||||
}
|
||||
}
|
||||
|
@ -6,7 +6,7 @@ use crate::{h_stack, Icon, IconElement, IconSize, Label};
|
||||
#[derive(IntoElement)]
|
||||
pub struct ListSubHeader {
|
||||
label: SharedString,
|
||||
left_icon: Option<Icon>,
|
||||
start_slot: Option<Icon>,
|
||||
inset: bool,
|
||||
}
|
||||
|
||||
@ -14,13 +14,13 @@ impl ListSubHeader {
|
||||
pub fn new(label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
label: label.into(),
|
||||
left_icon: None,
|
||||
start_slot: None,
|
||||
inset: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
|
||||
self.left_icon = left_icon;
|
||||
self.start_slot = left_icon;
|
||||
self
|
||||
}
|
||||
}
|
||||
@ -44,7 +44,7 @@ impl RenderOnce for ListSubHeader {
|
||||
.flex()
|
||||
.gap_1()
|
||||
.items_center()
|
||||
.children(self.left_icon.map(|i| {
|
||||
.children(self.start_slot.map(|i| {
|
||||
IconElement::new(i)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small)
|
||||
|
@ -15,19 +15,19 @@ impl Render for ListHeaderStory {
|
||||
.child(Story::label("Default"))
|
||||
.child(ListHeader::new("Section 1"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(ListHeader::new("Section 2").left_icon(Icon::Bell))
|
||||
.child(ListHeader::new("Section 2").start_slot(IconElement::new(Icon::Bell)))
|
||||
.child(Story::label("With left icon and meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 3")
|
||||
.left_icon(Icon::BellOff)
|
||||
.meta(IconButton::new("action_1", Icon::Bolt)),
|
||||
.start_slot(IconElement::new(Icon::BellOff))
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt)),
|
||||
)
|
||||
.child(Story::label("With multiple meta"))
|
||||
.child(
|
||||
ListHeader::new("Section 4")
|
||||
.meta(IconButton::new("action_1", Icon::Bolt))
|
||||
.meta(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||
.meta(IconButton::new("action_3", Icon::Plus)),
|
||||
.end_slot(IconButton::new("action_1", Icon::Bolt))
|
||||
.end_slot(IconButton::new("action_2", Icon::ExclamationTriangle))
|
||||
.end_slot(IconButton::new("action_3", Icon::Plus)),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
use gpui::{Div, Render};
|
||||
use story::Story;
|
||||
|
||||
use crate::prelude::*;
|
||||
use crate::{prelude::*, Avatar};
|
||||
use crate::{Icon, ListItem};
|
||||
|
||||
pub struct ListItemStory;
|
||||
@ -9,24 +9,80 @@ pub struct ListItemStory;
|
||||
impl Render for ListItemStory {
|
||||
type Element = Div;
|
||||
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
|
||||
Story::container()
|
||||
.bg(cx.theme().colors().background)
|
||||
.child(Story::title_for::<ListItem>())
|
||||
.child(Story::label("Default"))
|
||||
.child(ListItem::new("hello_world").child("Hello, world!"))
|
||||
.child(Story::label("With left icon"))
|
||||
.child(Story::label("Inset"))
|
||||
.child(
|
||||
ListItem::new("with_left_icon")
|
||||
ListItem::new("inset_list_item")
|
||||
.inset(true)
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child("Hello, world!")
|
||||
.left_icon(Icon::Bell),
|
||||
.end_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With left avatar"))
|
||||
.child(Story::label("With start slot icon"))
|
||||
.child(
|
||||
ListItem::new("with start slot_icon")
|
||||
.child("Hello, world!")
|
||||
.start_slot(
|
||||
IconElement::new(Icon::Bell)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
)
|
||||
.child(Story::label("With start slot avatar"))
|
||||
.child(
|
||||
ListItem::new("with_start slot avatar")
|
||||
.child("Hello, world!")
|
||||
.start_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end slot"))
|
||||
.child(
|
||||
ListItem::new("with_left_avatar")
|
||||
.child("Hello, world!")
|
||||
.left_avatar(SharedString::from(
|
||||
.end_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
)),
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With end hover slot"))
|
||||
.child(
|
||||
ListItem::new("with_end_hover_slot")
|
||||
.child("Hello, world!")
|
||||
.end_slot(
|
||||
h_stack()
|
||||
.gap_2()
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
)))
|
||||
.child(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1789?v=4",
|
||||
))),
|
||||
)
|
||||
.end_hover_slot(Avatar::new(SharedString::from(
|
||||
"https://avatars.githubusercontent.com/u/1714999?v=4",
|
||||
))),
|
||||
)
|
||||
.child(Story::label("With `on_click`"))
|
||||
.child(
|
||||
|
@ -158,7 +158,6 @@ impl RenderOnce for Tab {
|
||||
)
|
||||
.child(
|
||||
h_stack()
|
||||
.invisible()
|
||||
.w_3()
|
||||
.h_3()
|
||||
.justify_center()
|
||||
@ -167,7 +166,7 @@ impl RenderOnce for Tab {
|
||||
TabCloseSide::Start => this.left_1(),
|
||||
TabCloseSide::End => this.right_1(),
|
||||
})
|
||||
.group_hover("", |style| style.visible())
|
||||
.visible_on_hover("")
|
||||
.children(self.end_slot),
|
||||
)
|
||||
.children(self.children),
|
||||
|
@ -9,6 +9,7 @@ pub use crate::clickable::*;
|
||||
pub use crate::disableable::*;
|
||||
pub use crate::fixed::*;
|
||||
pub use crate::selectable::*;
|
||||
pub use crate::visible_on_hover::*;
|
||||
pub use crate::{h_stack, v_stack};
|
||||
pub use crate::{Button, ButtonSize, ButtonStyle, IconButton};
|
||||
pub use crate::{ButtonCommon, Color, StyledExt};
|
||||
|
@ -118,16 +118,26 @@ pub trait StyledExt: Styled + Sized {
|
||||
elevated(self, cx, ElevationIndex::ModalSurface)
|
||||
}
|
||||
|
||||
/// The theme's primary border color.
|
||||
fn border_primary(self, cx: &mut WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border)
|
||||
}
|
||||
|
||||
/// The theme's secondary or muted border color.
|
||||
fn border_muted(self, cx: &mut WindowContext) -> Self {
|
||||
self.border_color(cx.theme().colors().border_variant)
|
||||
}
|
||||
|
||||
fn debug_bg_red(self) -> Self {
|
||||
self.bg(gpui::red())
|
||||
self.bg(hsla(0. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_green(self) -> Self {
|
||||
self.bg(gpui::green())
|
||||
self.bg(hsla(120. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_blue(self) -> Self {
|
||||
self.bg(gpui::blue())
|
||||
self.bg(hsla(240. / 360., 1., 0.5, 1.))
|
||||
}
|
||||
|
||||
fn debug_bg_yellow(self) -> Self {
|
||||
|
@ -21,6 +21,7 @@ mod selectable;
|
||||
mod styled_ext;
|
||||
mod styles;
|
||||
pub mod utils;
|
||||
mod visible_on_hover;
|
||||
|
||||
pub use clickable::*;
|
||||
pub use components::*;
|
||||
@ -30,3 +31,4 @@ pub use prelude::*;
|
||||
pub use selectable::*;
|
||||
pub use styled_ext::*;
|
||||
pub use styles::*;
|
||||
pub use visible_on_hover::*;
|
||||
|
15
crates/ui2/src/visible_on_hover.rs
Normal file
15
crates/ui2/src/visible_on_hover.rs
Normal file
@ -0,0 +1,15 @@
|
||||
use gpui::{InteractiveElement, SharedString, Styled};
|
||||
|
||||
pub trait VisibleOnHover {
|
||||
/// Sets the element to only be visible when the specified group is hovered.
|
||||
///
|
||||
/// Pass `""` as the `group_name` to use the global group.
|
||||
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self;
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement + Styled> VisibleOnHover for E {
|
||||
fn visible_on_hover(self, group_name: impl Into<SharedString>) -> Self {
|
||||
self.invisible()
|
||||
.group_hover(group_name, |style| style.visible())
|
||||
}
|
||||
}
|
@ -1759,6 +1759,34 @@ impl Pane {
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn handle_split_tab_drop(
|
||||
&mut self,
|
||||
dragged_tab: &View<DraggedTab>,
|
||||
split_direction: SplitDirection,
|
||||
cx: &mut ViewContext<'_, Pane>,
|
||||
) {
|
||||
let dragged_tab = dragged_tab.read(cx);
|
||||
let item_id = dragged_tab.item_id;
|
||||
let from_pane = dragged_tab.pane.clone();
|
||||
let to_pane = cx.view().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
cx.defer(move |workspace, cx| {
|
||||
let item = from_pane
|
||||
.read(cx)
|
||||
.items()
|
||||
.find(|item| item.item_id() == item_id)
|
||||
.map(|item| item.boxed_clone());
|
||||
if let Some(item) = item {
|
||||
if let Some(item) = item.clone_on_split(workspace.database_id(), cx) {
|
||||
workspace.split_item(split_direction, item, cx);
|
||||
}
|
||||
}
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableView for Pane {
|
||||
@ -1852,7 +1880,54 @@ impl Render for Pane {
|
||||
.child(self.render_tab_bar(cx))
|
||||
.child(self.toolbar.clone())
|
||||
.child(if let Some(item) = self.active_item() {
|
||||
div().flex().flex_1().child(item.to_any())
|
||||
let mut drag_target_color = cx.theme().colors().text;
|
||||
drag_target_color.a = 0.5;
|
||||
|
||||
div()
|
||||
.flex()
|
||||
.flex_1()
|
||||
.relative()
|
||||
.child(item.to_any())
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.full()
|
||||
.z_index(1)
|
||||
.drag_over::<DraggedTab>(|style| style.bg(drag_target_color))
|
||||
.on_drop(cx.listener(
|
||||
move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||
this.handle_tab_drop(dragged_tab, this.active_item_index(), cx)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.children(
|
||||
[
|
||||
(SplitDirection::Up, 2),
|
||||
(SplitDirection::Down, 2),
|
||||
(SplitDirection::Left, 3),
|
||||
(SplitDirection::Right, 3),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(direction, z_index)| {
|
||||
let div = div()
|
||||
.absolute()
|
||||
.z_index(z_index)
|
||||
.invisible()
|
||||
.bg(drag_target_color)
|
||||
.drag_over::<DraggedTab>(|style| style.visible())
|
||||
.on_drop(cx.listener(
|
||||
move |this, dragged_tab: &View<DraggedTab>, cx| {
|
||||
this.handle_split_tab_drop(dragged_tab, direction, cx)
|
||||
},
|
||||
));
|
||||
match direction {
|
||||
SplitDirection::Up => div.top_0().left_0().right_0().h_32(),
|
||||
SplitDirection::Down => div.left_0().bottom_0().right_0().h_32(),
|
||||
SplitDirection::Left => div.top_0().left_0().bottom_0().w_32(),
|
||||
SplitDirection::Right => div.top_0().bottom_0().right_0().w_32(),
|
||||
}
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
h_stack()
|
||||
.items_center()
|
||||
|
@ -55,7 +55,7 @@ outline = { package = "outline2", path = "../outline2" }
|
||||
# plugin_runtime = { path = "../plugin_runtime",optional = true }
|
||||
project = { package = "project2", path = "../project2" }
|
||||
project_panel = { package = "project_panel2", path = "../project_panel2" }
|
||||
# project_symbols = { path = "../project_symbols" }
|
||||
project_symbols = { package = "project_symbols2", path = "../project_symbols2" }
|
||||
quick_action_bar = { package = "quick_action_bar2", path = "../quick_action_bar2" }
|
||||
recent_projects = { package = "recent_projects2", path = "../recent_projects2" }
|
||||
rope = { package = "rope2", path = "../rope2"}
|
||||
|
@ -205,7 +205,7 @@ fn main() {
|
||||
go_to_line::init(cx);
|
||||
file_finder::init(cx);
|
||||
outline::init(cx);
|
||||
// project_symbols::init(cx);
|
||||
project_symbols::init(cx);
|
||||
project_panel::init(Assets, cx);
|
||||
channel::init(&client, user_store.clone(), cx);
|
||||
// diagnostics::init(cx);
|
||||
|
1
docker-compose.sql
Normal file
1
docker-compose.sql
Normal file
@ -0,0 +1 @@
|
||||
create database zed;
|
16
docker-compose.yml
Normal file
16
docker-compose.yml
Normal file
@ -0,0 +1,16 @@
|
||||
version: "3.7"
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:15
|
||||
container_name: zed_postgres
|
||||
ports:
|
||||
- 5432:5432
|
||||
environment:
|
||||
POSTGRES_HOST_AUTH_METHOD: trust
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- ./docker-compose.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
15
script/storybook
Executable file
15
script/storybook
Executable file
@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
# This script takes a single text input and replaces 'list_item' with the input in a cargo run command
|
||||
|
||||
# Check if an argument is provided
|
||||
if [ "$#" -ne 1 ]; then
|
||||
echo "Usage: $0 <component_name>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Assign the argument to a variable
|
||||
COMPONENT_NAME="$1"
|
||||
|
||||
# Run the cargo command with the provided component name
|
||||
cargo run -p storybook2 -- components/"$COMPONENT_NAME"
|
Loading…
Reference in New Issue
Block a user