Detailed changes
@@ -2143,6 +2143,25 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "copilot_button2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "copilot2",
+ "editor2",
+ "fs2",
+ "futures 0.3.28",
+ "gpui2",
+ "language2",
+ "settings2",
+ "smol",
+ "theme2",
+ "util",
+ "workspace2",
+ "zed_actions2",
+]
+
[[package]]
name = "core-foundation"
version = "0.9.3"
@@ -4791,6 +4810,24 @@ dependencies = [
"workspace",
]
+[[package]]
+name = "language_selector2"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "editor2",
+ "fuzzy2",
+ "gpui2",
+ "language2",
+ "picker2",
+ "project2",
+ "settings2",
+ "theme2",
+ "ui2",
+ "util",
+ "workspace2",
+]
+
[[package]]
name = "language_tools"
version = "0.1.0"
@@ -11753,6 +11790,7 @@ dependencies = [
"collections",
"command_palette2",
"copilot2",
+ "copilot_button2",
"ctor",
"db2",
"diagnostics2",
@@ -11772,6 +11810,7 @@ dependencies = [
"isahc",
"journal2",
"language2",
+ "language_selector2",
"lazy_static",
"libc",
"log",
@@ -61,6 +61,7 @@ members = [
"crates/language",
"crates/language2",
"crates/language_selector",
+ "crates/language_selector2",
"crates/language_tools",
"crates/live_kit_client",
"crates/live_kit_server",
@@ -1,5 +1,5 @@
#![allow(unused)]
-// mod channel_modal;
+mod channel_modal;
mod contact_finder;
// use crate::{
@@ -192,6 +192,8 @@ use workspace::{
use crate::{face_pile::FacePile, CollaborationPanelSettings};
+use self::channel_modal::ChannelModal;
+
pub fn init(cx: &mut AppContext) {
cx.observe_new_views(|workspace: &mut Workspace, _| {
workspace.register_action(|workspace, _: &ToggleFocus, cx| {
@@ -2058,13 +2060,11 @@ impl CollabPanel {
}
fn invite_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- todo!();
- // self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
+ self.show_channel_modal(channel_id, channel_modal::Mode::InviteMembers, cx);
}
fn manage_members(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
- todo!();
- // self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
+ self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
@@ -2156,38 +2156,36 @@ impl CollabPanel {
})
}
- // fn show_channel_modal(
- // &mut self,
- // channel_id: ChannelId,
- // mode: channel_modal::Mode,
- // cx: &mut ViewContext<Self>,
- // ) {
- // let workspace = self.workspace.clone();
- // let user_store = self.user_store.clone();
- // let channel_store = self.channel_store.clone();
- // let members = self.channel_store.update(cx, |channel_store, cx| {
- // channel_store.get_channel_member_details(channel_id, cx)
- // });
-
- // cx.spawn(|_, mut cx| async move {
- // let members = members.await?;
- // workspace.update(&mut cx, |workspace, cx| {
- // workspace.toggle_modal(cx, |_, cx| {
- // cx.add_view(|cx| {
- // ChannelModal::new(
- // user_store.clone(),
- // channel_store.clone(),
- // channel_id,
- // mode,
- // members,
- // cx,
- // )
- // })
- // });
- // })
- // })
- // .detach();
- // }
+ fn show_channel_modal(
+ &mut self,
+ channel_id: ChannelId,
+ mode: channel_modal::Mode,
+ cx: &mut ViewContext<Self>,
+ ) {
+ let workspace = self.workspace.clone();
+ let user_store = self.user_store.clone();
+ let channel_store = self.channel_store.clone();
+ let members = self.channel_store.update(cx, |channel_store, cx| {
+ channel_store.get_channel_member_details(channel_id, cx)
+ });
+
+ cx.spawn(|_, mut cx| async move {
+ let members = members.await?;
+ workspace.update(&mut cx, |workspace, cx| {
+ workspace.toggle_modal(cx, |cx| {
+ ChannelModal::new(
+ user_store.clone(),
+ channel_store.clone(),
+ channel_id,
+ mode,
+ members,
+ cx,
+ )
+ });
+ })
+ })
+ .detach();
+ }
// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext<Self>) {
// self.remove_channel(action.channel_id, cx)
@@ -3,58 +3,54 @@ use client::{
proto::{self, ChannelRole, ChannelVisibility},
User, UserId, UserStore,
};
-use context_menu::{ContextMenu, ContextMenuItem};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
- actions,
- elements::*,
- platform::{CursorStyle, MouseButton},
- AppContext, ClipboardItem, Entity, ModelHandle, MouseState, Task, View, ViewContext,
- ViewHandle,
+ actions, div, AppContext, ClipboardItem, DismissEvent, Div, Entity, EventEmitter,
+ FocusableView, Model, ParentElement, Render, Styled, Task, View, ViewContext, VisualContext,
+ WeakView,
};
-use picker::{Picker, PickerDelegate, PickerEvent};
+use picker::{Picker, PickerDelegate};
use std::sync::Arc;
+use ui::v_stack;
use util::TryFutureExt;
-use workspace::Modal;
actions!(
- channel_modal,
- [
- SelectNextControl,
- ToggleMode,
- ToggleMemberAdmin,
- RemoveMember
- ]
+ SelectNextControl,
+ ToggleMode,
+ ToggleMemberAdmin,
+ RemoveMember
);
-pub fn init(cx: &mut AppContext) {
- Picker::<ChannelModalDelegate>::init(cx);
- cx.add_action(ChannelModal::toggle_mode);
- cx.add_action(ChannelModal::toggle_member_admin);
- cx.add_action(ChannelModal::remove_member);
- cx.add_action(ChannelModal::dismiss);
-}
+// pub fn init(cx: &mut AppContext) {
+// Picker::<ChannelModalDelegate>::init(cx);
+// cx.add_action(ChannelModal::toggle_mode);
+// cx.add_action(ChannelModal::toggle_member_admin);
+// cx.add_action(ChannelModal::remove_member);
+// cx.add_action(ChannelModal::dismiss);
+// }
pub struct ChannelModal {
- picker: ViewHandle<Picker<ChannelModalDelegate>>,
- channel_store: ModelHandle<ChannelStore>,
+ picker: View<Picker<ChannelModalDelegate>>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
has_focus: bool,
}
impl ChannelModal {
pub fn new(
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
mode: Mode,
members: Vec<ChannelMembership>,
cx: &mut ViewContext<Self>,
) -> Self {
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
- let picker = cx.add_view(|cx| {
+ let channel_modal = cx.view().downgrade();
+ let picker = cx.build_view(|cx| {
Picker::new(
ChannelModalDelegate {
+ channel_modal,
matching_users: Vec::new(),
matching_member_indices: Vec::new(),
selected_index: 0,
@@ -64,20 +60,17 @@ impl ChannelModal {
match_candidates: Vec::new(),
members,
mode,
- context_menu: cx.add_view(|cx| {
- let mut menu = ContextMenu::new(cx.view_id(), cx);
- menu.set_position_mode(OverlayPositionMode::Local);
- menu
- }),
+ // context_menu: cx.add_view(|cx| {
+ // let mut menu = ContextMenu::new(cx.view_id(), cx);
+ // menu.set_position_mode(OverlayPositionMode::Local);
+ // menu
+ // }),
},
cx,
)
- .with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
});
- cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
-
- let has_focus = picker.read(cx).has_focus();
+ let has_focus = picker.focus_handle(cx).contains_focused(cx);
Self {
picker,
@@ -88,7 +81,7 @@ impl ChannelModal {
}
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
- let mode = match self.picker.read(cx).delegate().mode {
+ let mode = match self.picker.read(cx).delegate.mode {
Mode::ManageMembers => Mode::InviteMembers,
Mode::InviteMembers => Mode::ManageMembers,
};
@@ -103,20 +96,20 @@ impl ChannelModal {
let mut members = channel_store
.update(&mut cx, |channel_store, cx| {
channel_store.get_channel_member_details(channel_id, cx)
- })
+ })?
.await?;
members.sort_by(|a, b| a.sort_key().cmp(&b.sort_key()));
this.update(&mut cx, |this, cx| {
this.picker
- .update(cx, |picker, _| picker.delegate_mut().members = members);
+ .update(cx, |picker, _| picker.delegate.members = members);
})?;
}
this.update(&mut cx, |this, cx| {
this.picker.update(cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.mode = mode;
delegate.selected_index = 0;
picker.set_query("", cx);
@@ -131,203 +124,194 @@ impl ChannelModal {
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
- picker.delegate_mut().toggle_selected_member_admin(cx);
+ picker.delegate.toggle_selected_member_admin(cx);
})
}
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
self.picker.update(cx, |picker, cx| {
- picker.delegate_mut().remove_selected_member(cx);
+ picker.delegate.remove_selected_member(cx);
});
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
- cx.emit(PickerEvent::Dismiss);
+ cx.emit(DismissEvent);
}
}
-impl Entity for ChannelModal {
- type Event = PickerEvent;
-}
-
-impl View for ChannelModal {
- fn ui_name() -> &'static str {
- "ChannelModal"
- }
-
- fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
- let theme = &theme::current(cx).collab_panel.tabbed_modal;
-
- let mode = self.picker.read(cx).delegate().mode;
- let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
- return Empty::new().into_any();
- };
-
- enum InviteMembers {}
- enum ManageMembers {}
-
- fn render_mode_button<T: 'static>(
- mode: Mode,
- text: &'static str,
- current_mode: Mode,
- theme: &theme::TabbedModal,
- cx: &mut ViewContext<ChannelModal>,
- ) -> AnyElement<ChannelModal> {
- let active = mode == current_mode;
- MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
- let contained_text = theme.tab_button.style_for(active, state);
- Label::new(text, contained_text.text.clone())
- .contained()
- .with_style(contained_text.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if !active {
- this.set_mode(mode, cx);
- }
- })
- .with_cursor_style(CursorStyle::PointingHand)
- .into_any()
- }
-
- fn render_visibility(
- channel_id: ChannelId,
- visibility: ChannelVisibility,
- theme: &theme::TabbedModal,
- cx: &mut ViewContext<ChannelModal>,
- ) -> AnyElement<ChannelModal> {
- enum TogglePublic {}
-
- if visibility == ChannelVisibility::Members {
- return Flex::row()
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
- let style = theme.visibility_toggle.style_for(state);
- Label::new(format!("{}", "Public access: OFF"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.set_channel_visibility(
- channel_id,
- ChannelVisibility::Public,
- cx,
- )
- })
- .detach_and_log_err(cx);
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .into_any();
- }
-
- Flex::row()
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
- let style = theme.visibility_toggle.style_for(state);
- Label::new(format!("{}", "Public access: ON"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- this.channel_store
- .update(cx, |channel_store, cx| {
- channel_store.set_channel_visibility(
- channel_id,
- ChannelVisibility::Members,
- cx,
- )
- })
- .detach_and_log_err(cx);
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .with_spacing(14.0)
- .with_child(
- MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
- let style = theme.channel_link.style_for(state);
- Label::new(format!("{}", "copy link"), style.text.clone())
- .contained()
- .with_style(style.container.clone())
- })
- .on_click(MouseButton::Left, move |_, this, cx| {
- if let Some(channel) =
- this.channel_store.read(cx).channel_for_id(channel_id)
- {
- let item = ClipboardItem::new(channel.link());
- cx.write_to_clipboard(item);
- }
- })
- .with_cursor_style(CursorStyle::PointingHand),
- )
- .into_any()
- }
-
- Flex::column()
- .with_child(
- Flex::column()
- .with_child(
- Label::new(format!("#{}", channel.name), theme.title.text.clone())
- .contained()
- .with_style(theme.title.container.clone()),
- )
- .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
- .with_child(Flex::row().with_children([
- render_mode_button::<InviteMembers>(
- Mode::InviteMembers,
- "Invite members",
- mode,
- theme,
- cx,
- ),
- render_mode_button::<ManageMembers>(
- Mode::ManageMembers,
- "Manage members",
- mode,
- theme,
- cx,
- ),
- ]))
- .expanded()
- .contained()
- .with_style(theme.header),
- )
- .with_child(
- ChildView::new(&self.picker, cx)
- .contained()
- .with_style(theme.body),
- )
- .constrained()
- .with_max_height(theme.max_height)
- .with_max_width(theme.max_width)
- .contained()
- .with_style(theme.modal)
- .into_any()
- }
-
- fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
- self.has_focus = true;
- if cx.is_self_focused() {
- cx.focus(&self.picker)
- }
- }
+impl EventEmitter<DismissEvent> for ChannelModal {}
- fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
- self.has_focus = false;
+impl FocusableView for ChannelModal {
+ fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
+ self.picker.focus_handle(cx)
}
}
-impl Modal for ChannelModal {
- fn has_focus(&self) -> bool {
- self.has_focus
+impl Render for ChannelModal {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ // let theme = &theme::current(cx).collab_panel.tabbed_modal;
+
+ // let mode = self.picker.read(cx).delegate().mode;
+ // let Some(channel) = self.channel_store.read(cx).channel_for_id(self.channel_id) else {
+ // return Empty::new().into_any();
+ // };
+
+ // enum InviteMembers {}
+ // enum ManageMembers {}
+
+ // fn render_mode_button<T: 'static>(
+ // mode: Mode,
+ // text: &'static str,
+ // current_mode: Mode,
+ // theme: &theme::TabbedModal,
+ // cx: &mut ViewContext<ChannelModal>,
+ // ) -> AnyElement<ChannelModal> {
+ // let active = mode == current_mode;
+ // MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
+ // let contained_text = theme.tab_button.style_for(active, state);
+ // Label::new(text, contained_text.text.clone())
+ // .contained()
+ // .with_style(contained_text.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if !active {
+ // this.set_mode(mode, cx);
+ // }
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand)
+ // .into_any()
+ // }
+
+ // fn render_visibility(
+ // channel_id: ChannelId,
+ // visibility: ChannelVisibility,
+ // theme: &theme::TabbedModal,
+ // cx: &mut ViewContext<ChannelModal>,
+ // ) -> AnyElement<ChannelModal> {
+ // enum TogglePublic {}
+
+ // if visibility == ChannelVisibility::Members {
+ // return Flex::row()
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+ // let style = theme.visibility_toggle.style_for(state);
+ // Label::new(format!("{}", "Public access: OFF"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.channel_store
+ // .update(cx, |channel_store, cx| {
+ // channel_store.set_channel_visibility(
+ // channel_id,
+ // ChannelVisibility::Public,
+ // cx,
+ // )
+ // })
+ // .detach_and_log_err(cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .into_any();
+ // }
+
+ // Flex::row()
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(0, cx, move |state, _| {
+ // let style = theme.visibility_toggle.style_for(state);
+ // Label::new(format!("{}", "Public access: ON"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // this.channel_store
+ // .update(cx, |channel_store, cx| {
+ // channel_store.set_channel_visibility(
+ // channel_id,
+ // ChannelVisibility::Members,
+ // cx,
+ // )
+ // })
+ // .detach_and_log_err(cx);
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .with_spacing(14.0)
+ // .with_child(
+ // MouseEventHandler::new::<TogglePublic, _>(1, cx, move |state, _| {
+ // let style = theme.channel_link.style_for(state);
+ // Label::new(format!("{}", "copy link"), style.text.clone())
+ // .contained()
+ // .with_style(style.container.clone())
+ // })
+ // .on_click(MouseButton::Left, move |_, this, cx| {
+ // if let Some(channel) =
+ // this.channel_store.read(cx).channel_for_id(channel_id)
+ // {
+ // let item = ClipboardItem::new(channel.link());
+ // cx.write_to_clipboard(item);
+ // }
+ // })
+ // .with_cursor_style(CursorStyle::PointingHand),
+ // )
+ // .into_any()
+ // }
+
+ // Flex::column()
+ // .with_child(
+ // Flex::column()
+ // .with_child(
+ // Label::new(format!("#{}", channel.name), theme.title.text.clone())
+ // .contained()
+ // .with_style(theme.title.container.clone()),
+ // )
+ // .with_child(render_visibility(channel.id, channel.visibility, theme, cx))
+ // .with_child(Flex::row().with_children([
+ // render_mode_button::<InviteMembers>(
+ // Mode::InviteMembers,
+ // "Invite members",
+ // mode,
+ // theme,
+ // cx,
+ // ),
+ // render_mode_button::<ManageMembers>(
+ // Mode::ManageMembers,
+ // "Manage members",
+ // mode,
+ // theme,
+ // cx,
+ // ),
+ // ]))
+ // .expanded()
+ // .contained()
+ // .with_style(theme.header),
+ // )
+ // .with_child(
+ // ChildView::new(&self.picker, cx)
+ // .contained()
+ // .with_style(theme.body),
+ // )
+ // .constrained()
+ // .with_max_height(theme.max_height)
+ // .with_max_width(theme.max_width)
+ // .contained()
+ // .with_style(theme.modal)
+ // .into_any()
}
- fn dismiss_on_event(event: &Self::Event) -> bool {
- match event {
- PickerEvent::Dismiss => true,
- }
- }
+ // fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
+ // self.has_focus = true;
+ // if cx.is_self_focused() {
+ // cx.focus(&self.picker)
+ // }
+ // }
+
+ // fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
+ // self.has_focus = false;
+ // }
}
#[derive(Copy, Clone, PartialEq)]
@@ -337,19 +321,22 @@ pub enum Mode {
}
pub struct ChannelModalDelegate {
+ channel_modal: WeakView<ChannelModal>,
matching_users: Vec<Arc<User>>,
matching_member_indices: Vec<usize>,
- user_store: ModelHandle<UserStore>,
- channel_store: ModelHandle<ChannelStore>,
+ user_store: Model<UserStore>,
+ channel_store: Model<ChannelStore>,
channel_id: ChannelId,
selected_index: usize,
mode: Mode,
match_candidates: Vec<StringMatchCandidate>,
members: Vec<ChannelMembership>,
- context_menu: ViewHandle<ContextMenu>,
+ // context_menu: ViewHandle<ContextMenu>,
}
impl PickerDelegate for ChannelModalDelegate {
+ type ListItem = Div;
+
fn placeholder_text(&self) -> Arc<str> {
"Search collaborator by username...".into()
}
@@ -382,19 +369,19 @@ impl PickerDelegate for ChannelModalDelegate {
}
}));
- let matches = cx.background().block(match_strings(
+ let matches = cx.background_executor().block(match_strings(
&self.match_candidates,
&query,
true,
usize::MAX,
&Default::default(),
- cx.background().clone(),
+ cx.background_executor().clone(),
));
cx.spawn(|picker, mut cx| async move {
picker
.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
+ let delegate = &mut picker.delegate;
delegate.matching_member_indices.clear();
delegate
.matching_member_indices
@@ -412,8 +399,7 @@ impl PickerDelegate for ChannelModalDelegate {
async {
let users = search_users.await?;
picker.update(&mut cx, |picker, cx| {
- let delegate = picker.delegate_mut();
- delegate.matching_users = users;
+ picker.delegate.matching_users = users;
cx.notify();
})?;
anyhow::Ok(())
@@ -445,138 +431,142 @@ impl PickerDelegate for ChannelModalDelegate {
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
- cx.emit(PickerEvent::Dismiss);
+ self.channel_modal
+ .update(cx, |_, cx| {
+ cx.emit(DismissEvent);
+ })
+ .ok();
}
fn render_match(
&self,
ix: usize,
- mouse_state: &mut MouseState,
selected: bool,
- cx: &gpui::AppContext,
- ) -> AnyElement<Picker<Self>> {
- let full_theme = &theme::current(cx);
- let theme = &full_theme.collab_panel.channel_modal;
- let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
- let (user, role) = self.user_at_index(ix).unwrap();
- let request_status = self.member_status(user.id, cx);
-
- let style = tabbed_modal
- .picker
- .item
- .in_state(selected)
- .style_for(mouse_state);
-
- let in_manage = matches!(self.mode, Mode::ManageMembers);
-
- let mut result = Flex::row()
- .with_children(user.avatar.clone().map(|avatar| {
- Image::from_data(avatar)
- .with_style(theme.contact_avatar)
- .aligned()
- .left()
- }))
- .with_child(
- Label::new(user.github_login.clone(), style.label.clone())
- .contained()
- .with_style(theme.contact_username)
- .aligned()
- .left(),
- )
- .with_children({
- (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
- || {
- Label::new("Invited", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left()
- },
- )
- })
- .with_children(if in_manage && role == Some(ChannelRole::Admin) {
- Some(
- Label::new("Admin", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left(),
- )
- } else if in_manage && role == Some(ChannelRole::Guest) {
- Some(
- Label::new("Guest", theme.member_tag.text.clone())
- .contained()
- .with_style(theme.member_tag.container)
- .aligned()
- .left(),
- )
- } else {
- None
- })
- .with_children({
- let svg = match self.mode {
- Mode::ManageMembers => Some(
- Svg::new("icons/ellipsis.svg")
- .with_color(theme.member_icon.color)
- .constrained()
- .with_width(theme.member_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.member_icon.button_width)
- .with_height(theme.member_icon.button_width)
- .contained()
- .with_style(theme.member_icon.container),
- ),
- Mode::InviteMembers => match request_status {
- Some(proto::channel_member::Kind::Member) => Some(
- Svg::new("icons/check.svg")
- .with_color(theme.member_icon.color)
- .constrained()
- .with_width(theme.member_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.member_icon.button_width)
- .with_height(theme.member_icon.button_width)
- .contained()
- .with_style(theme.member_icon.container),
- ),
- Some(proto::channel_member::Kind::Invitee) => Some(
- Svg::new("icons/check.svg")
- .with_color(theme.invitee_icon.color)
- .constrained()
- .with_width(theme.invitee_icon.icon_width)
- .aligned()
- .constrained()
- .with_width(theme.invitee_icon.button_width)
- .with_height(theme.invitee_icon.button_width)
- .contained()
- .with_style(theme.invitee_icon.container),
- ),
- Some(proto::channel_member::Kind::AncestorMember) | None => None,
- },
- };
-
- svg.map(|svg| svg.aligned().flex_float().into_any())
- })
- .contained()
- .with_style(style.container)
- .constrained()
- .with_height(tabbed_modal.row_height)
- .into_any();
-
- if selected {
- result = Stack::new()
- .with_child(result)
- .with_child(
- ChildView::new(&self.context_menu, cx)
- .aligned()
- .top()
- .right(),
- )
- .into_any();
- }
-
- result
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ None
+ // let full_theme = &theme::current(cx);
+ // let theme = &full_theme.collab_panel.channel_modal;
+ // let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
+ // let (user, role) = self.user_at_index(ix).unwrap();
+ // let request_status = self.member_status(user.id, cx);
+
+ // let style = tabbed_modal
+ // .picker
+ // .item
+ // .in_state(selected)
+ // .style_for(mouse_state);
+
+ // let in_manage = matches!(self.mode, Mode::ManageMembers);
+
+ // let mut result = Flex::row()
+ // .with_children(user.avatar.clone().map(|avatar| {
+ // Image::from_data(avatar)
+ // .with_style(theme.contact_avatar)
+ // .aligned()
+ // .left()
+ // }))
+ // .with_child(
+ // Label::new(user.github_login.clone(), style.label.clone())
+ // .contained()
+ // .with_style(theme.contact_username)
+ // .aligned()
+ // .left(),
+ // )
+ // .with_children({
+ // (in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
+ // || {
+ // Label::new("Invited", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left()
+ // },
+ // )
+ // })
+ // .with_children(if in_manage && role == Some(ChannelRole::Admin) {
+ // Some(
+ // Label::new("Admin", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left(),
+ // )
+ // } else if in_manage && role == Some(ChannelRole::Guest) {
+ // Some(
+ // Label::new("Guest", theme.member_tag.text.clone())
+ // .contained()
+ // .with_style(theme.member_tag.container)
+ // .aligned()
+ // .left(),
+ // )
+ // } else {
+ // None
+ // })
+ // .with_children({
+ // let svg = match self.mode {
+ // Mode::ManageMembers => Some(
+ // Svg::new("icons/ellipsis.svg")
+ // .with_color(theme.member_icon.color)
+ // .constrained()
+ // .with_width(theme.member_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.member_icon.button_width)
+ // .with_height(theme.member_icon.button_width)
+ // .contained()
+ // .with_style(theme.member_icon.container),
+ // ),
+ // Mode::InviteMembers => match request_status {
+ // Some(proto::channel_member::Kind::Member) => Some(
+ // Svg::new("icons/check.svg")
+ // .with_color(theme.member_icon.color)
+ // .constrained()
+ // .with_width(theme.member_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.member_icon.button_width)
+ // .with_height(theme.member_icon.button_width)
+ // .contained()
+ // .with_style(theme.member_icon.container),
+ // ),
+ // Some(proto::channel_member::Kind::Invitee) => Some(
+ // Svg::new("icons/check.svg")
+ // .with_color(theme.invitee_icon.color)
+ // .constrained()
+ // .with_width(theme.invitee_icon.icon_width)
+ // .aligned()
+ // .constrained()
+ // .with_width(theme.invitee_icon.button_width)
+ // .with_height(theme.invitee_icon.button_width)
+ // .contained()
+ // .with_style(theme.invitee_icon.container),
+ // ),
+ // Some(proto::channel_member::Kind::AncestorMember) | None => None,
+ // },
+ // };
+
+ // svg.map(|svg| svg.aligned().flex_float().into_any())
+ // })
+ // .contained()
+ // .with_style(style.container)
+ // .constrained()
+ // .with_height(tabbed_modal.row_height)
+ // .into_any();
+
+ // if selected {
+ // result = Stack::new()
+ // .with_child(result)
+ // .with_child(
+ // ChildView::new(&self.context_menu, cx)
+ // .aligned()
+ // .top()
+ // .right(),
+ // )
+ // .into_any();
+ // }
+
+ // result
}
}
@@ -623,7 +613,7 @@ impl ChannelModalDelegate {
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
- let this = picker.delegate_mut();
+ let this = &mut picker.delegate;
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
member.role = new_role;
}
@@ -644,7 +634,7 @@ impl ChannelModalDelegate {
cx.spawn(|picker, mut cx| async move {
update.await?;
picker.update(&mut cx, |picker, cx| {
- let this = picker.delegate_mut();
+ let this = &mut picker.delegate;
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
this.members.remove(ix);
this.matching_member_indices.retain_mut(|member_ix| {
@@ -683,7 +673,7 @@ impl ChannelModalDelegate {
kind: proto::channel_member::Kind::Invitee,
role: ChannelRole::Member,
};
- let members = &mut this.delegate_mut().members;
+ let members = &mut this.delegate.members;
match members.binary_search_by_key(&new_member.sort_key(), |k| k.sort_key()) {
Ok(ix) | Err(ix) => members.insert(ix, new_member),
}
@@ -695,23 +685,23 @@ impl ChannelModalDelegate {
}
fn show_context_menu(&mut self, role: ChannelRole, cx: &mut ViewContext<Picker<Self>>) {
- self.context_menu.update(cx, |context_menu, cx| {
- context_menu.show(
- Default::default(),
- AnchorCorner::TopRight,
- vec![
- ContextMenuItem::action("Remove", RemoveMember),
- ContextMenuItem::action(
- if role == ChannelRole::Admin {
- "Make non-admin"
- } else {
- "Make admin"
- },
- ToggleMemberAdmin,
- ),
- ],
- cx,
- )
- })
+ // self.context_menu.update(cx, |context_menu, cx| {
+ // context_menu.show(
+ // Default::default(),
+ // AnchorCorner::TopRight,
+ // vec![
+ // ContextMenuItem::action("Remove", RemoveMember),
+ // ContextMenuItem::action(
+ // if role == ChannelRole::Admin {
+ // "Make non-admin"
+ // } else {
+ // "Make admin"
+ // },
+ // ToggleMemberAdmin,
+ // ),
+ // ],
+ // cx,
+ // )
+ // })
}
}
@@ -0,0 +1,27 @@
+[package]
+name = "copilot_button2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/copilot_button.rs"
+doctest = false
+
+[dependencies]
+copilot = { package = "copilot2", path = "../copilot2" }
+editor = { package = "editor2", path = "../editor2" }
+fs = { package = "fs2", path = "../fs2" }
+zed-actions = { package="zed_actions2", path = "../zed_actions2"}
+gpui = { package = "gpui2", path = "../gpui2" }
+language = { package = "language2", path = "../language2" }
+settings = { package = "settings2", path = "../settings2" }
+theme = { package = "theme2", path = "../theme2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+smol.workspace = true
+futures.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -0,0 +1,371 @@
+#![allow(unused)]
+use anyhow::Result;
+use copilot::{Copilot, SignOut, Status};
+use editor::{scroll::autoscroll::Autoscroll, Editor};
+use fs::Fs;
+use gpui::{
+ div, Action, AnchorCorner, AppContext, AsyncAppContext, AsyncWindowContext, Div, Entity,
+ ParentElement, Render, Subscription, View, ViewContext, WeakView, WindowContext,
+};
+use language::{
+ language_settings::{self, all_language_settings, AllLanguageSettings},
+ File, Language,
+};
+use settings::{update_settings_file, Settings, SettingsStore};
+use std::{path::Path, sync::Arc};
+use util::{paths, ResultExt};
+use workspace::{
+ create_and_open_local_file,
+ item::ItemHandle,
+ ui::{
+ popover_menu, ButtonCommon, Clickable, ContextMenu, Icon, IconButton, PopoverMenu, Tooltip,
+ },
+ StatusItemView, Toast, Workspace,
+};
+use zed_actions::OpenBrowser;
+
+const COPILOT_SETTINGS_URL: &str = "https://github.com/settings/copilot";
+const COPILOT_STARTING_TOAST_ID: usize = 1337;
+const COPILOT_ERROR_TOAST_ID: usize = 1338;
+
+pub struct CopilotButton {
+ editor_subscription: Option<(Subscription, usize)>,
+ editor_enabled: Option<bool>,
+ language: Option<Arc<Language>>,
+ file: Option<Arc<dyn File>>,
+ fs: Arc<dyn Fs>,
+}
+
+impl Render for CopilotButton {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
+ let all_language_settings = all_language_settings(None, cx);
+ if !all_language_settings.copilot.feature_enabled {
+ return div();
+ }
+
+ let Some(copilot) = Copilot::global(cx) else {
+ return div();
+ };
+ let status = copilot.read(cx).status();
+
+ let enabled = self
+ .editor_enabled
+ .unwrap_or_else(|| all_language_settings.copilot_enabled(None, None));
+
+ let icon = match status {
+ Status::Error(_) => Icon::CopilotError,
+ Status::Authorized => {
+ if enabled {
+ Icon::Copilot
+ } else {
+ Icon::CopilotDisabled
+ }
+ }
+ _ => Icon::CopilotInit,
+ };
+
+ if let Status::Error(e) = status {
+ return div().child(
+ IconButton::new("copilot-error", icon)
+ .on_click(cx.listener(move |this, _, cx| {
+ if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+ workspace.update(cx, |workspace, cx| {
+ workspace.show_toast(
+ Toast::new(
+ COPILOT_ERROR_TOAST_ID,
+ format!("Copilot can't be started: {}", e),
+ )
+ .on_click(
+ "Reinstall Copilot",
+ |cx| {
+ if let Some(copilot) = Copilot::global(cx) {
+ copilot
+ .update(cx, |copilot, cx| copilot.reinstall(cx))
+ .detach();
+ }
+ },
+ ),
+ cx,
+ );
+ });
+ }
+ }))
+ .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+ );
+ }
+ let this = cx.view().clone();
+
+ div().child(
+ popover_menu("copilot")
+ .menu(move |cx| match status {
+ Status::Authorized => this.update(cx, |this, cx| this.build_copilot_menu(cx)),
+ _ => this.update(cx, |this, cx| this.build_copilot_start_menu(cx)),
+ })
+ .anchor(AnchorCorner::BottomRight)
+ .trigger(
+ IconButton::new("copilot-icon", icon)
+ .tooltip(|cx| Tooltip::text("GitHub Copilot", cx)),
+ ),
+ )
+ }
+}
+
+impl CopilotButton {
+ pub fn new(fs: Arc<dyn Fs>, cx: &mut ViewContext<Self>) -> Self {
+ Copilot::global(cx).map(|copilot| cx.observe(&copilot, |_, _, cx| cx.notify()).detach());
+
+ cx.observe_global::<SettingsStore>(move |_, cx| cx.notify())
+ .detach();
+
+ Self {
+ editor_subscription: None,
+ editor_enabled: None,
+ language: None,
+ file: None,
+ fs,
+ }
+ }
+
+ pub fn build_copilot_start_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+ let fs = self.fs.clone();
+ ContextMenu::build(cx, |menu, cx| {
+ menu.entry("Sign In", initiate_sign_in)
+ .entry("Disable Copilot", move |cx| hide_copilot(fs.clone(), cx))
+ })
+ }
+
+ pub fn build_copilot_menu(&mut self, cx: &mut ViewContext<Self>) -> View<ContextMenu> {
+ let fs = self.fs.clone();
+
+ return ContextMenu::build(cx, move |mut menu, cx| {
+ if let Some(language) = self.language.clone() {
+ let fs = fs.clone();
+ let language_enabled =
+ language_settings::language_settings(Some(&language), None, cx)
+ .show_copilot_suggestions;
+
+ menu = menu.entry(
+ format!(
+ "{} Suggestions for {}",
+ if language_enabled { "Hide" } else { "Show" },
+ language.name()
+ ),
+ move |cx| toggle_copilot_for_language(language.clone(), fs.clone(), cx),
+ );
+ }
+
+ let settings = AllLanguageSettings::get_global(cx);
+
+ if let Some(file) = &self.file {
+ let path = file.path().clone();
+ let path_enabled = settings.copilot_enabled_for_path(&path);
+
+ menu = menu.entry(
+ format!(
+ "{} Suggestions for This Path",
+ if path_enabled { "Hide" } else { "Show" }
+ ),
+ move |cx| {
+ if let Some(workspace) = cx.window_handle().downcast::<Workspace>() {
+ if let Ok(workspace) = workspace.root_view(cx) {
+ let workspace = workspace.downgrade();
+ cx.spawn(|cx| {
+ configure_disabled_globs(
+ workspace,
+ path_enabled.then_some(path.clone()),
+ cx,
+ )
+ })
+ .detach_and_log_err(cx);
+ }
+ }
+ },
+ );
+ }
+
+ let globally_enabled = settings.copilot_enabled(None, None);
+ menu.entry(
+ if globally_enabled {
+ "Hide Suggestions for All Files"
+ } else {
+ "Show Suggestions for All Files"
+ },
+ move |cx| toggle_copilot_globally(fs.clone(), cx),
+ )
+ .separator()
+ .link(
+ "Copilot Settings",
+ OpenBrowser {
+ url: COPILOT_SETTINGS_URL.to_string(),
+ }
+ .boxed_clone(),
+ cx,
+ )
+ .action("Sign Out", SignOut.boxed_clone(), cx)
+ });
+ }
+
+ pub fn update_enabled(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ let editor = editor.read(cx);
+ let snapshot = editor.buffer().read(cx).snapshot(cx);
+ let suggestion_anchor = editor.selections.newest_anchor().start;
+ let language = snapshot.language_at(suggestion_anchor);
+ let file = snapshot.file_at(suggestion_anchor).cloned();
+
+ self.editor_enabled = Some(
+ all_language_settings(self.file.as_ref(), cx)
+ .copilot_enabled(language, file.as_ref().map(|file| file.path().as_ref())),
+ );
+ self.language = language.cloned();
+ self.file = file;
+
+ cx.notify()
+ }
+}
+
+impl StatusItemView for CopilotButton {
+ fn set_active_pane_item(&mut self, item: Option<&dyn ItemHandle>, cx: &mut ViewContext<Self>) {
+ if let Some(editor) = item.map(|item| item.act_as::<Editor>(cx)).flatten() {
+ self.editor_subscription = Some((
+ cx.observe(&editor, Self::update_enabled),
+ editor.entity_id().as_u64() as usize,
+ ));
+ self.update_enabled(editor, cx);
+ } else {
+ self.language = None;
+ self.editor_subscription = None;
+ self.editor_enabled = None;
+ }
+ cx.notify();
+ }
+}
+
+async fn configure_disabled_globs(
+ workspace: WeakView<Workspace>,
+ path_to_disable: Option<Arc<Path>>,
+ mut cx: AsyncWindowContext,
+) -> Result<()> {
+ let settings_editor = workspace
+ .update(&mut cx, |_, cx| {
+ create_and_open_local_file(&paths::SETTINGS, cx, || {
+ settings::initial_user_settings_content().as_ref().into()
+ })
+ })?
+ .await?
+ .downcast::<Editor>()
+ .unwrap();
+
+ settings_editor.downgrade().update(&mut cx, |item, cx| {
+ let text = item.buffer().read(cx).snapshot(cx).text();
+
+ let settings = cx.global::<SettingsStore>();
+ let edits = settings.edits_for_update::<AllLanguageSettings>(&text, |file| {
+ let copilot = file.copilot.get_or_insert_with(Default::default);
+ let globs = copilot.disabled_globs.get_or_insert_with(|| {
+ settings
+ .get::<AllLanguageSettings>(None)
+ .copilot
+ .disabled_globs
+ .iter()
+ .map(|glob| glob.glob().to_string())
+ .collect()
+ });
+
+ if let Some(path_to_disable) = &path_to_disable {
+ globs.push(path_to_disable.to_string_lossy().into_owned());
+ } else {
+ globs.clear();
+ }
+ });
+
+ if !edits.is_empty() {
+ item.change_selections(Some(Autoscroll::newest()), cx, |selections| {
+ selections.select_ranges(edits.iter().map(|e| e.0.clone()));
+ });
+
+ // When *enabling* a path, don't actually perform an edit, just select the range.
+ if path_to_disable.is_some() {
+ item.edit(edits.iter().cloned(), cx);
+ }
+ }
+ })?;
+
+ anyhow::Ok(())
+}
+
+fn toggle_copilot_globally(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions = all_language_settings(None, cx).copilot_enabled(None, None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.defaults.show_copilot_suggestions = Some((!show_copilot_suggestions).into())
+ });
+}
+
+fn toggle_copilot_for_language(language: Arc<Language>, fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ let show_copilot_suggestions =
+ all_language_settings(None, cx).copilot_enabled(Some(&language), None);
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.languages
+ .entry(language.name())
+ .or_default()
+ .show_copilot_suggestions = Some(!show_copilot_suggestions);
+ });
+}
+
+fn hide_copilot(fs: Arc<dyn Fs>, cx: &mut AppContext) {
+ update_settings_file::<AllLanguageSettings>(fs, cx, move |file| {
+ file.features.get_or_insert(Default::default()).copilot = Some(false);
+ });
+}
+
+fn initiate_sign_in(cx: &mut WindowContext) {
+ let Some(copilot) = Copilot::global(cx) else {
+ return;
+ };
+ let status = copilot.read(cx).status();
+
+ match status {
+ Status::Starting { task } => {
+ let Some(workspace) = cx.window_handle().downcast::<Workspace>() else {
+ return;
+ };
+
+ let Ok(workspace) = workspace.update(cx, |workspace, cx| {
+ workspace.show_toast(
+ Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot is starting..."),
+ cx,
+ );
+ workspace.weak_handle()
+ }) else {
+ return;
+ };
+
+ cx.spawn(|mut cx| async move {
+ task.await;
+ if let Some(copilot) = cx.update(|_, cx| Copilot::global(cx)).ok().flatten() {
+ workspace
+ .update(&mut cx, |workspace, cx| match copilot.read(cx).status() {
+ Status::Authorized => workspace.show_toast(
+ Toast::new(COPILOT_STARTING_TOAST_ID, "Copilot has started!"),
+ cx,
+ ),
+ _ => {
+ workspace.dismiss_toast(COPILOT_STARTING_TOAST_ID, cx);
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .detach_and_log_err(cx);
+ }
+ })
+ .log_err();
+ }
+ })
+ .detach();
+ }
+ _ => {
+ copilot
+ .update(cx, |copilot, cx| copilot.sign_in(cx))
+ .detach_and_log_err(cx);
+ }
+ }
+}
@@ -1920,14 +1920,14 @@ impl Editor {
// self.buffer.read(cx).read(cx).file_at(point).cloned()
// }
- // pub fn active_excerpt(
- // &self,
- // cx: &AppContext,
- // ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
- // self.buffer
- // .read(cx)
- // .excerpt_containing(self.selections.newest_anchor().head(), cx)
- // }
+ pub fn active_excerpt(
+ &self,
+ cx: &AppContext,
+ ) -> Option<(ExcerptId, Model<Buffer>, Range<text::Anchor>)> {
+ self.buffer
+ .read(cx)
+ .excerpt_containing(self.selections.newest_anchor().head(), cx)
+ }
// pub fn style(&self, cx: &AppContext) -> EditorStyle {
// build_style(
@@ -992,10 +992,6 @@ impl Interactivity {
let interactive_bounds = interactive_bounds.clone();
cx.on_mouse_event(move |event: &MouseMoveEvent, phase, cx| {
- if phase != DispatchPhase::Bubble {
- return;
- }
-
let is_hovered = interactive_bounds.visibly_contains(&event.position, cx)
&& pending_mouse_down.borrow().is_none();
if !is_hovered {
@@ -1003,6 +999,10 @@ impl Interactivity {
return;
}
+ if phase != DispatchPhase::Bubble {
+ return;
+ }
+
if active_tooltip.borrow().is_none() {
let task = cx.spawn({
let active_tooltip = active_tooltip.clone();
@@ -0,0 +1,26 @@
+[package]
+name = "language_selector2"
+version = "0.1.0"
+edition = "2021"
+publish = false
+
+[lib]
+path = "src/language_selector.rs"
+doctest = false
+
+[dependencies]
+editor = { package = "editor2", path = "../editor2" }
+fuzzy = { package = "fuzzy2", path = "../fuzzy2" }
+language = { package = "language2", path = "../language2" }
+gpui = { package = "gpui2", path = "../gpui2" }
+picker = { package = "picker2", path = "../picker2" }
+project = { package = "project2", path = "../project2" }
+theme = { package = "theme2", path = "../theme2" }
+ui = { package = "ui2", path = "../ui2" }
+settings = { package = "settings2", path = "../settings2" }
+util = { path = "../util" }
+workspace = { package = "workspace2", path = "../workspace2" }
+anyhow.workspace = true
+
+[dev-dependencies]
+editor = { package = "editor2", path = "../editor2", features = ["test-support"] }
@@ -0,0 +1,82 @@
+use editor::Editor;
+use gpui::{
+ div, Div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView,
+};
+use std::sync::Arc;
+use ui::{Button, ButtonCommon, Clickable, Tooltip};
+use workspace::{item::ItemHandle, StatusItemView, Workspace};
+
+use crate::LanguageSelector;
+
+pub struct ActiveBufferLanguage {
+ active_language: Option<Option<Arc<str>>>,
+ workspace: WeakView<Workspace>,
+ _observe_active_editor: Option<Subscription>,
+}
+
+impl ActiveBufferLanguage {
+ pub fn new(workspace: &Workspace) -> Self {
+ Self {
+ active_language: None,
+ workspace: workspace.weak_handle(),
+ _observe_active_editor: None,
+ }
+ }
+
+ fn update_language(&mut self, editor: View<Editor>, cx: &mut ViewContext<Self>) {
+ self.active_language = Some(None);
+
+ let editor = editor.read(cx);
+ if let Some((_, buffer, _)) = editor.active_excerpt(cx) {
+ if let Some(language) = buffer.read(cx).language() {
+ self.active_language = Some(Some(language.name()));
+ }
+ }
+
+ cx.notify();
+ }
+}
+
+impl Render for ActiveBufferLanguage {
+ type Element = Div;
+
+ fn render(&mut self, cx: &mut ViewContext<Self>) -> Div {
+ div().when_some(self.active_language.as_ref(), |el, active_language| {
+ let active_language_text = if let Some(active_language_text) = active_language {
+ active_language_text.to_string()
+ } else {
+ "Unknown".to_string()
+ };
+
+ el.child(
+ Button::new("change-language", active_language_text)
+ .on_click(cx.listener(|this, _, cx| {
+ if let Some(workspace) = this.workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ LanguageSelector::toggle(workspace, cx)
+ });
+ }
+ }))
+ .tooltip(|cx| Tooltip::text("Select Language", cx)),
+ )
+ })
+ }
+}
+
+impl StatusItemView for ActiveBufferLanguage {
+ fn set_active_pane_item(
+ &mut self,
+ active_pane_item: Option<&dyn ItemHandle>,
+ cx: &mut ViewContext<Self>,
+ ) {
+ if let Some(editor) = active_pane_item.and_then(|item| item.act_as::<Editor>(cx)) {
+ self._observe_active_editor = Some(cx.observe(&editor, Self::update_language));
+ self.update_language(editor, cx);
+ } else {
+ self.active_language = None;
+ self._observe_active_editor = None;
+ }
+
+ cx.notify();
+ }
+}
@@ -0,0 +1,231 @@
+mod active_buffer_language;
+
+pub use active_buffer_language::ActiveBufferLanguage;
+use anyhow::anyhow;
+use editor::Editor;
+use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
+use gpui::{
+ actions, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView, Model,
+ ParentElement, Render, Styled, View, ViewContext, VisualContext, WeakView,
+};
+use language::{Buffer, LanguageRegistry};
+use picker::{Picker, PickerDelegate};
+use project::Project;
+use std::sync::Arc;
+use ui::{v_stack, HighlightedLabel, ListItem, Selectable};
+use util::ResultExt;
+use workspace::Workspace;
+
+actions!(Toggle);
+
+pub fn init(cx: &mut AppContext) {
+ cx.observe_new_views(LanguageSelector::register).detach();
+}
+
+pub struct LanguageSelector {
+ picker: View<Picker<LanguageSelectorDelegate>>,
+}
+
+impl LanguageSelector {
+ fn register(workspace: &mut Workspace, _: &mut ViewContext<Workspace>) {
+ workspace.register_action(move |workspace, _: &Toggle, cx| {
+ Self::toggle(workspace, cx);
+ });
+ }
+
+ fn toggle(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> Option<()> {
+ let registry = workspace.app_state().languages.clone();
+ let (_, buffer, _) = workspace
+ .active_item(cx)?
+ .act_as::<Editor>(cx)?
+ .read(cx)
+ .active_excerpt(cx)?;
+ let project = workspace.project().clone();
+
+ workspace.toggle_modal(cx, move |cx| {
+ LanguageSelector::new(buffer, project, registry, cx)
+ });
+ Some(())
+ }
+
+ fn new(
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut ViewContext<Self>,
+ ) -> Self {
+ let delegate = LanguageSelectorDelegate::new(
+ cx.view().downgrade(),
+ buffer,
+ project,
+ language_registry,
+ );
+
+ let picker = cx.build_view(|cx| Picker::new(delegate, cx));
+ Self { picker }
+ }
+}
+
+impl Render for LanguageSelector {
+ type Element = Div;
+
+ fn render(&mut self, _cx: &mut ViewContext<Self>) -> Self::Element {
+ v_stack().min_w_96().child(self.picker.clone())
+ }
+}
+
+impl FocusableView for LanguageSelector {
+ fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
+ self.picker.focus_handle(cx)
+ }
+}
+impl EventEmitter<DismissEvent> for LanguageSelector {}
+
+pub struct LanguageSelectorDelegate {
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ candidates: Vec<StringMatchCandidate>,
+ matches: Vec<StringMatch>,
+ selected_index: usize,
+}
+
+impl LanguageSelectorDelegate {
+ fn new(
+ language_selector: WeakView<LanguageSelector>,
+ buffer: Model<Buffer>,
+ project: Model<Project>,
+ language_registry: Arc<LanguageRegistry>,
+ ) -> Self {
+ let candidates = language_registry
+ .language_names()
+ .into_iter()
+ .enumerate()
+ .map(|(candidate_id, name)| StringMatchCandidate::new(candidate_id, name))
+ .collect::<Vec<_>>();
+
+ Self {
+ language_selector,
+ buffer,
+ project,
+ language_registry,
+ candidates,
+ matches: vec![],
+ selected_index: 0,
+ }
+ }
+}
+
+impl PickerDelegate for LanguageSelectorDelegate {
+ type ListItem = ListItem;
+
+ fn placeholder_text(&self) -> Arc<str> {
+ "Select a language...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
+ if let Some(mat) = self.matches.get(self.selected_index) {
+ let language_name = &self.candidates[mat.candidate_id].string;
+ let language = self.language_registry.language_for_name(language_name);
+ let project = self.project.downgrade();
+ let buffer = self.buffer.downgrade();
+ cx.spawn(|_, mut cx| async move {
+ let language = language.await?;
+ let project = project
+ .upgrade()
+ .ok_or_else(|| anyhow!("project was dropped"))?;
+ let buffer = buffer
+ .upgrade()
+ .ok_or_else(|| anyhow!("buffer was dropped"))?;
+ project.update(&mut cx, |project, cx| {
+ project.set_language_for_buffer(&buffer, language, cx);
+ })
+ })
+ .detach_and_log_err(cx);
+ }
+ self.dismissed(cx);
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ self.language_selector
+ .update(cx, |_, cx| cx.emit(DismissEvent))
+ .log_err();
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> gpui::Task<()> {
+ let background = cx.background_executor().clone();
+ let candidates = self.candidates.clone();
+ 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| {
+ let delegate = &mut this.delegate;
+ delegate.matches = matches;
+ delegate.selected_index = delegate
+ .selected_index
+ .min(delegate.matches.len().saturating_sub(1));
+ cx.notify();
+ })
+ .log_err();
+ })
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ let mat = &self.matches[ix];
+ let buffer_language_name = self.buffer.read(cx).language().map(|l| l.name());
+ let mut label = mat.string.clone();
+ if buffer_language_name.as_deref() == Some(mat.string.as_str()) {
+ label.push_str(" (current)");
+ }
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .selected(selected)
+ .child(HighlightedLabel::new(label, mat.positions.clone())),
+ )
+ }
+}
@@ -178,6 +178,15 @@ impl<D: PickerDelegate> Picker<D> {
}
cx.notify();
}
+
+ pub fn query(&self, cx: &AppContext) -> String {
+ self.editor.read(cx).text(cx)
+ }
+
+ pub fn set_query(&self, query: impl Into<Arc<str>>, cx: &mut ViewContext<Self>) {
+ self.editor
+ .update(cx, |editor, cx| editor.set_text(query, cx));
+ }
}
impl<D: PickerDelegate> Render for Picker<D> {
@@ -1,5 +1,6 @@
use crate::{
- h_stack, prelude::*, v_stack, KeyBinding, Label, List, ListItem, ListSeparator, ListSubHeader,
+ h_stack, prelude::*, v_stack, Icon, IconElement, KeyBinding, Label, List, ListItem,
+ ListSeparator, ListSubHeader,
};
use gpui::{
px, Action, AppContext, DismissEvent, Div, EventEmitter, FocusHandle, FocusableView,
@@ -13,6 +14,7 @@ pub enum ContextMenuItem {
Header(SharedString),
Entry {
label: SharedString,
+ icon: Option<Icon>,
handler: Rc<dyn Fn(&mut WindowContext)>,
key_binding: Option<KeyBinding>,
},
@@ -69,6 +71,7 @@ impl ContextMenu {
label: label.into(),
handler: Rc::new(on_click),
key_binding: None,
+ icon: None,
});
self
}
@@ -83,6 +86,22 @@ impl ContextMenu {
label: label.into(),
key_binding: KeyBinding::for_action(&*action, cx),
handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+ icon: None,
+ });
+ self
+ }
+
+ pub fn link(
+ mut self,
+ label: impl Into<SharedString>,
+ action: Box<dyn Action>,
+ cx: &mut WindowContext,
+ ) -> Self {
+ self.items.push(ContextMenuItem::Entry {
+ label: label.into(),
+ key_binding: KeyBinding::for_action(&*action, cx),
+ handler: Rc::new(move |cx| cx.dispatch_action(action.boxed_clone())),
+ icon: Some(Icon::Link),
});
self
}
@@ -175,19 +194,30 @@ impl Render for ContextMenu {
ListSubHeader::new(header.clone()).into_any_element()
}
ContextMenuItem::Entry {
- label: entry,
- handler: callback,
+ label,
+ handler,
key_binding,
+ icon,
} => {
- let callback = callback.clone();
+ let handler = handler.clone();
let dismiss = cx.listener(|_, _, cx| cx.emit(DismissEvent));
- ListItem::new(entry.clone())
+ let label_element = if let Some(icon) = icon {
+ h_stack()
+ .gap_1()
+ .child(Label::new(label.clone()))
+ .child(IconElement::new(*icon))
+ .into_any_element()
+ } else {
+ Label::new(label.clone()).into_any_element()
+ };
+
+ ListItem::new(label.clone())
.child(
h_stack()
.w_full()
.justify_between()
- .child(Label::new(entry.clone()))
+ .child(label_element)
.children(
key_binding
.clone()
@@ -196,7 +226,7 @@ impl Render for ContextMenu {
)
.selected(Some(ix) == self.selected_index)
.on_click(move |event, cx| {
- callback(cx);
+ handler(cx);
dismiss(event, cx)
})
.into_any_element()
@@ -54,6 +54,7 @@ pub enum Icon {
FolderX,
Hash,
InlayHint,
+ Link,
MagicWand,
MagnifyingGlass,
MailOpen,
@@ -126,6 +127,7 @@ impl Icon {
Icon::FolderX => "icons/stop_sharing.svg",
Icon::Hash => "icons/hash.svg",
Icon::InlayHint => "icons/inlay_hint.svg",
+ Icon::Link => "icons/link.svg",
Icon::MagicWand => "icons/magic-wand.svg",
Icon::MagnifyingGlass => "icons/magnifying_glass.svg",
Icon::MailOpen => "icons/mail-open.svg",
@@ -135,24 +135,22 @@ impl Workspace {
}
pub fn show_toast(&mut self, toast: Toast, cx: &mut ViewContext<Self>) {
- todo!()
- // self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
- // self.show_notification(toast.id, cx, |cx| {
- // cx.add_view(|_cx| match toast.on_click.as_ref() {
- // Some((click_msg, on_click)) => {
- // let on_click = on_click.clone();
- // simple_message_notification::MessageNotification::new(toast.msg.clone())
- // .with_click_message(click_msg.clone())
- // .on_click(move |cx| on_click(cx))
- // }
- // None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
- // })
- // })
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(toast.id, cx);
+ self.show_notification(toast.id, cx, |cx| {
+ cx.build_view(|_cx| match toast.on_click.as_ref() {
+ Some((click_msg, on_click)) => {
+ let on_click = on_click.clone();
+ simple_message_notification::MessageNotification::new(toast.msg.clone())
+ .with_click_message(click_msg.clone())
+ .on_click(move |cx| on_click(cx))
+ }
+ None => simple_message_notification::MessageNotification::new(toast.msg.clone()),
+ })
+ })
}
pub fn dismiss_toast(&mut self, id: usize, cx: &mut ViewContext<Self>) {
- todo!()
- // self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
+ self.dismiss_notification::<simple_message_notification::MessageNotification>(id, cx);
}
fn dismiss_notification_internal(
@@ -179,33 +177,10 @@ pub mod simple_message_notification {
ParentElement, Render, SharedString, StatefulInteractiveElement, Styled, TextStyle,
ViewContext,
};
- use serde::Deserialize;
- use std::{borrow::Cow, sync::Arc};
+ use std::sync::Arc;
use ui::prelude::*;
use ui::{h_stack, v_stack, Button, Icon, IconElement, Label, StyledExt};
- #[derive(Clone, Default, Deserialize, PartialEq)]
- pub struct OsOpen(pub Cow<'static, str>);
-
- impl OsOpen {
- pub fn new<I: Into<Cow<'static, str>>>(url: I) -> Self {
- OsOpen(url.into())
- }
- }
-
- // todo!()
- // impl_actions!(message_notifications, [OsOpen]);
- //
- // todo!()
- // pub fn init(cx: &mut AppContext) {
- // cx.add_action(MessageNotification::dismiss);
- // cx.add_action(
- // |_workspace: &mut Workspace, open_action: &OsOpen, cx: &mut ViewContext<Workspace>| {
- // cx.platform().open_url(open_action.0.as_ref());
- // },
- // )
- // }
-
enum NotificationMessage {
Text(SharedString),
Element(fn(TextStyle, &AppContext) -> AnyElement),
@@ -213,7 +188,7 @@ pub mod simple_message_notification {
pub struct MessageNotification {
message: NotificationMessage,
- on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>) + Send + Sync>>,
+ on_click: Option<Arc<dyn Fn(&mut ViewContext<Self>)>>,
click_message: Option<SharedString>,
}
@@ -252,7 +227,7 @@ pub mod simple_message_notification {
pub fn on_click<F>(mut self, on_click: F) -> Self
where
- F: 'static + Send + Sync + Fn(&mut ViewContext<Self>),
+ F: 'static + Fn(&mut ViewContext<Self>),
{
self.on_click = Some(Arc::new(on_click));
self
@@ -6,7 +6,7 @@ use gpui::{
WindowContext,
};
use ui::prelude::*;
-use ui::{h_stack, Button, Icon, IconButton};
+use ui::{h_stack, Icon, IconButton};
use util::ResultExt;
pub trait StatusItemView: Render {
@@ -53,39 +53,11 @@ impl Render for StatusBar {
.gap_4()
.child(
h_stack().gap_1().child(
- // TODO: Language picker
+ // Feedback Tool
div()
.border()
.border_color(gpui::red())
- .child(Button::new("status_buffer_language", "Rust")),
- ),
- )
- .child(
- h_stack()
- .gap_1()
- .child(
- // Github tool
- div()
- .border()
- .border_color(gpui::red())
- .child(IconButton::new("status-copilot", Icon::Copilot)),
- )
- .child(
- // Feedback Tool
- div()
- .border()
- .border_color(gpui::red())
- .child(IconButton::new("status-feedback", Icon::Envelope)),
- ),
- )
- .child(
- // Bottom Dock
- h_stack().gap_1().child(
- // Terminal
- div()
- .border()
- .border_color(gpui::red())
- .child(IconButton::new("status-terminal", Icon::Terminal)),
+ .child(IconButton::new("status-feedback", Icon::Envelope)),
),
)
.child(
@@ -30,7 +30,7 @@ command_palette = { package="command_palette2", path = "../command_palette2" }
client = { package = "client2", path = "../client2" }
# clock = { path = "../clock" }
copilot = { package = "copilot2", path = "../copilot2" }
-# copilot_button = { path = "../copilot_button" }
+copilot_button = { package = "copilot_button2", path = "../copilot_button2" }
diagnostics = { package = "diagnostics2", path = "../diagnostics2" }
db = { package = "db2", path = "../db2" }
editor = { package="editor2", path = "../editor2" }
@@ -44,7 +44,7 @@ gpui = { package = "gpui2", path = "../gpui2" }
install_cli = { package = "install_cli2", path = "../install_cli2" }
journal = { package = "journal2", path = "../journal2" }
language = { package = "language2", path = "../language2" }
-# language_selector = { path = "../language_selector" }
+language_selector = { package = "language_selector2", path = "../language_selector2" }
lsp = { package = "lsp2", path = "../lsp2" }
menu = { package = "menu2", path = "../menu2" }
# language_tools = { path = "../language_tools" }
@@ -216,7 +216,7 @@ fn main() {
terminal_view::init(cx);
// journal2::init(app_state.clone(), cx);
- // language_selector::init(cx);
+ language_selector::init(cx);
theme_selector::init(cx);
// activity_indicator::init(cx);
// language_tools::init(cx);
@@ -136,14 +136,14 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
// cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx));
// workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx);
- // let copilot =
- // cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
+ let copilot =
+ cx.build_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx));
let diagnostic_summary =
cx.build_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx));
let activity_indicator =
activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx);
- // let active_buffer_language =
- // cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
+ let active_buffer_language =
+ cx.build_view(|_| language_selector::ActiveBufferLanguage::new(workspace));
// let vim_mode_indicator = cx.add_view(|cx| vim::ModeIndicator::new(cx));
// let feedback_button = cx.add_view(|_| {
// feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)
@@ -154,8 +154,8 @@ pub fn initialize_workspace(app_state: Arc<AppState>, cx: &mut AppContext) {
status_bar.add_left_item(activity_indicator, cx);
// status_bar.add_right_item(feedback_button, cx);
- // status_bar.add_right_item(copilot, cx);
- // status_bar.add_right_item(active_buffer_language, cx);
+ status_bar.add_right_item(copilot, cx);
+ status_bar.add_right_item(active_buffer_language, cx);
// status_bar.add_right_item(vim_mode_indicator, cx);
status_bar.add_right_item(cursor_position, cx);
});