mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-27 04:44:30 +00:00
Start a call when clicking on a contact in the contacts popover
Co-Authored-By: Antonio Scandurra <antonio@zed.dev>
This commit is contained in:
parent
815cf44647
commit
8ff4f044b7
9 changed files with 229 additions and 53 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -1078,6 +1078,7 @@ dependencies = [
|
|||
"menu",
|
||||
"postage",
|
||||
"project",
|
||||
"room",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
|
@ -4461,6 +4462,7 @@ dependencies = [
|
|||
"futures",
|
||||
"gpui",
|
||||
"project",
|
||||
"util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
@ -83,7 +83,7 @@ async fn test_basic_calls(
|
|||
.await;
|
||||
|
||||
let room_a = cx_a
|
||||
.update(|cx| Room::create(client_a.clone(), cx))
|
||||
.update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
|
@ -125,7 +125,7 @@ async fn test_basic_calls(
|
|||
|
||||
// User B joins the room using the first client.
|
||||
let room_b = cx_b
|
||||
.update(|cx| Room::join(&call_b, client_b.clone(), cx))
|
||||
.update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(incoming_call_b.next().await.unwrap().is_none());
|
||||
|
@ -229,7 +229,7 @@ async fn test_leaving_room_on_disconnection(
|
|||
.await;
|
||||
|
||||
let room_a = cx_a
|
||||
.update(|cx| Room::create(client_a.clone(), cx))
|
||||
.update(|cx| Room::create(client_a.clone(), client_a.user_store.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -245,7 +245,7 @@ async fn test_leaving_room_on_disconnection(
|
|||
// User B receives the call and joins the room.
|
||||
let call_b = incoming_call_b.next().await.unwrap().unwrap();
|
||||
let room_b = cx_b
|
||||
.update(|cx| Room::join(&call_b, client_b.clone(), cx))
|
||||
.update(|cx| Room::join(&call_b, client_b.clone(), client_b.user_store.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
deterministic.run_until_parked();
|
||||
|
@ -6284,17 +6284,9 @@ async fn room_participants(
|
|||
.collect::<Vec<_>>()
|
||||
});
|
||||
let remote_users = futures::future::try_join_all(remote_users).await.unwrap();
|
||||
let pending_users = room.update(cx, |room, cx| {
|
||||
room.pending_user_ids()
|
||||
.iter()
|
||||
.map(|user_id| {
|
||||
client
|
||||
.user_store
|
||||
.update(cx, |users, cx| users.get_user(*user_id, cx))
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
let pending_users = room.read_with(cx, |room, _| {
|
||||
room.pending_users().iter().cloned().collect::<Vec<_>>()
|
||||
});
|
||||
let pending_users = futures::future::try_join_all(pending_users).await.unwrap();
|
||||
|
||||
RoomParticipants {
|
||||
remote: remote_users
|
||||
|
|
|
@ -14,6 +14,7 @@ test-support = [
|
|||
"editor/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"room/test-support",
|
||||
"settings/test-support",
|
||||
"util/test-support",
|
||||
"workspace/test-support",
|
||||
|
@ -28,6 +29,7 @@ fuzzy = { path = "../fuzzy" }
|
|||
gpui = { path = "../gpui" }
|
||||
menu = { path = "../menu" }
|
||||
project = { path = "../project" }
|
||||
room = { path = "../room" }
|
||||
settings = { path = "../settings" }
|
||||
theme = { path = "../theme" }
|
||||
util = { path = "../util" }
|
||||
|
@ -44,6 +46,7 @@ collections = { path = "../collections", features = ["test-support"] }
|
|||
editor = { path = "../editor", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
room = { path = "../room", features = ["test-support"] }
|
||||
settings = { path = "../settings", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
workspace = { path = "../workspace", features = ["test-support"] }
|
||||
|
|
|
@ -71,8 +71,9 @@ impl CollabTitlebarItem {
|
|||
Some(_) => {}
|
||||
None => {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
let client = workspace.read(cx).client().clone();
|
||||
let user_store = workspace.read(cx).user_store().clone();
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(user_store, cx));
|
||||
let view = cx.add_view(|cx| ContactsPopover::new(client, user_store, cx));
|
||||
cx.focus(&view);
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use client::{Contact, User, UserStore};
|
||||
use client::{Client, Contact, User, UserStore};
|
||||
use editor::{Cancel, Editor};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
|
@ -9,10 +9,11 @@ use gpui::{
|
|||
ViewHandle,
|
||||
};
|
||||
use menu::{Confirm, SelectNext, SelectPrev};
|
||||
use room::Room;
|
||||
use settings::Settings;
|
||||
use theme::IconButton;
|
||||
|
||||
impl_internal_actions!(contacts_panel, [ToggleExpanded]);
|
||||
impl_internal_actions!(contacts_panel, [ToggleExpanded, Call]);
|
||||
|
||||
pub fn init(cx: &mut MutableAppContext) {
|
||||
cx.add_action(ContactsPopover::clear_filter);
|
||||
|
@ -20,11 +21,17 @@ pub fn init(cx: &mut MutableAppContext) {
|
|||
cx.add_action(ContactsPopover::select_prev);
|
||||
cx.add_action(ContactsPopover::confirm);
|
||||
cx.add_action(ContactsPopover::toggle_expanded);
|
||||
cx.add_action(ContactsPopover::call);
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct ToggleExpanded(Section);
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct Call {
|
||||
recipient_user_id: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)]
|
||||
enum Section {
|
||||
Requests,
|
||||
|
@ -73,18 +80,24 @@ pub enum Event {
|
|||
}
|
||||
|
||||
pub struct ContactsPopover {
|
||||
room: Option<(ModelHandle<Room>, Subscription)>,
|
||||
entries: Vec<ContactEntry>,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
list_state: ListState,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
filter_editor: ViewHandle<Editor>,
|
||||
collapsed_sections: Vec<Section>,
|
||||
selection: Option<usize>,
|
||||
_maintain_contacts: Subscription,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ContactsPopover {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
pub fn new(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
let filter_editor = cx.add_view(|cx| {
|
||||
let mut editor = Editor::single_line(
|
||||
Some(|theme| theme.contacts_panel.user_query_editor.clone()),
|
||||
|
@ -143,25 +156,52 @@ impl ContactsPopover {
|
|||
cx,
|
||||
),
|
||||
ContactEntry::Contact(contact) => {
|
||||
Self::render_contact(&contact.user, &theme.contacts_panel, is_selected)
|
||||
Self::render_contact(contact, &theme.contacts_panel, is_selected, cx)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx)));
|
||||
|
||||
let weak_self = cx.weak_handle();
|
||||
subscriptions.push(Room::observe(cx, move |room, cx| {
|
||||
if let Some(this) = weak_self.upgrade(cx) {
|
||||
this.update(cx, |this, cx| this.set_room(room, cx));
|
||||
}
|
||||
}));
|
||||
|
||||
let mut this = Self {
|
||||
room: None,
|
||||
list_state,
|
||||
selection: None,
|
||||
collapsed_sections: Default::default(),
|
||||
entries: Default::default(),
|
||||
match_candidates: Default::default(),
|
||||
filter_editor,
|
||||
_maintain_contacts: cx.observe(&user_store, |this, _, cx| this.update_entries(cx)),
|
||||
_subscriptions: subscriptions,
|
||||
client,
|
||||
user_store,
|
||||
};
|
||||
this.update_entries(cx);
|
||||
this
|
||||
}
|
||||
|
||||
fn set_room(&mut self, room: Option<ModelHandle<Room>>, cx: &mut ViewContext<Self>) {
|
||||
if let Some(room) = room {
|
||||
let observation = cx.observe(&room, |this, room, cx| this.room_updated(room, cx));
|
||||
self.room = Some((room, observation));
|
||||
} else {
|
||||
self.room = None;
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn room_updated(&mut self, room: ModelHandle<Room>, cx: &mut ViewContext<Self>) {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn clear_filter(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
|
||||
let did_clear = self.filter_editor.update(cx, |editor, cx| {
|
||||
if editor.buffer().read(cx).len(cx) > 0 {
|
||||
|
@ -357,6 +397,43 @@ impl ContactsPopover {
|
|||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_active_call(&self, cx: &mut RenderContext<Self>) -> Option<ElementBox> {
|
||||
let (room, _) = self.room.as_ref()?;
|
||||
let theme = &cx.global::<Settings>().theme.contacts_panel;
|
||||
|
||||
Some(
|
||||
Flex::column()
|
||||
.with_children(room.read(cx).pending_users().iter().map(|user| {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
user.github_login.clone(),
|
||||
theme.contact_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(theme.contact_row.default)
|
||||
.boxed()
|
||||
}))
|
||||
.boxed(),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_header(
|
||||
section: Section,
|
||||
theme: &theme::ContactsPanel,
|
||||
|
@ -412,32 +489,46 @@ impl ContactsPopover {
|
|||
.boxed()
|
||||
}
|
||||
|
||||
fn render_contact(user: &User, theme: &theme::ContactsPanel, is_selected: bool) -> ElementBox {
|
||||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
fn render_contact(
|
||||
contact: &Contact,
|
||||
theme: &theme::ContactsPanel,
|
||||
is_selected: bool,
|
||||
cx: &mut RenderContext<Self>,
|
||||
) -> ElementBox {
|
||||
let user_id = contact.user.id;
|
||||
MouseEventHandler::<Contact>::new(contact.user.id as usize, cx, |_, _| {
|
||||
Flex::row()
|
||||
.with_children(contact.user.avatar.clone().map(|avatar| {
|
||||
Image::new(avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
contact.user.github_login.clone(),
|
||||
theme.contact_username.text.clone(),
|
||||
)
|
||||
.contained()
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.boxed()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(
|
||||
user.github_login.clone(),
|
||||
theme.contact_username.text.clone(),
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(theme.contact_username.container)
|
||||
.aligned()
|
||||
.left()
|
||||
.flex(1., true)
|
||||
.boxed(),
|
||||
)
|
||||
.constrained()
|
||||
.with_height(theme.row_height)
|
||||
.contained()
|
||||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.boxed()
|
||||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.boxed()
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, cx| {
|
||||
cx.dispatch_action(Call {
|
||||
recipient_user_id: user_id,
|
||||
})
|
||||
})
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn render_contact_request(
|
||||
|
@ -553,6 +644,21 @@ impl ContactsPopover {
|
|||
.with_style(*theme.contact_row.style_for(Default::default(), is_selected))
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn call(&mut self, action: &Call, cx: &mut ViewContext<Self>) {
|
||||
let client = self.client.clone();
|
||||
let user_store = self.user_store.clone();
|
||||
let recipient_user_id = action.recipient_user_id;
|
||||
cx.spawn_weak(|_, mut cx| async move {
|
||||
let room = cx
|
||||
.update(|cx| Room::get_or_create(&client, &user_store, cx))
|
||||
.await?;
|
||||
room.update(&mut cx, |room, cx| room.call(recipient_user_id, cx))
|
||||
.await?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactsPopover {
|
||||
|
@ -606,6 +712,7 @@ impl View for ContactsPopover {
|
|||
.with_height(theme.contacts_panel.user_query_editor_height)
|
||||
.boxed(),
|
||||
)
|
||||
.with_children(self.render_active_call(cx))
|
||||
.with_child(List::new(self.list_state.clone()).flex(1., false).boxed())
|
||||
.with_children(
|
||||
self.user_store
|
||||
|
|
|
@ -1519,6 +1519,17 @@ impl MutableAppContext {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn observe_default_global<G, F>(&mut self, observe: F) -> Subscription
|
||||
where
|
||||
G: Any + Default,
|
||||
F: 'static + FnMut(&mut MutableAppContext),
|
||||
{
|
||||
if !self.has_global::<G>() {
|
||||
self.set_global(G::default());
|
||||
}
|
||||
self.observe_global::<G, F>(observe)
|
||||
}
|
||||
|
||||
pub fn observe_release<E, H, F>(&mut self, handle: &H, callback: F) -> Subscription
|
||||
where
|
||||
E: Entity,
|
||||
|
|
|
@ -13,6 +13,7 @@ test-support = [
|
|||
"collections/test-support",
|
||||
"gpui/test-support",
|
||||
"project/test-support",
|
||||
"util/test-support"
|
||||
]
|
||||
|
||||
[dependencies]
|
||||
|
@ -20,6 +21,7 @@ client = { path = "../client" }
|
|||
collections = { path = "../collections" }
|
||||
gpui = { path = "../gpui" }
|
||||
project = { path = "../project" }
|
||||
util = { path = "../util" }
|
||||
|
||||
anyhow = "1.0.38"
|
||||
futures = "0.3"
|
||||
|
@ -29,3 +31,4 @@ client = { path = "../client", features = ["test-support"] }
|
|||
collections = { path = "../collections", features = ["test-support"] }
|
||||
gpui = { path = "../gpui", features = ["test-support"] }
|
||||
project = { path = "../project", features = ["test-support"] }
|
||||
util = { path = "../util", features = ["test-support"] }
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
mod participant;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use client::{call::Call, proto, Client, PeerId, TypedEnvelope};
|
||||
use client::{call::Call, proto, Client, PeerId, TypedEnvelope, User, UserStore};
|
||||
use collections::HashMap;
|
||||
use futures::StreamExt;
|
||||
use gpui::{AsyncAppContext, Entity, ModelContext, ModelHandle, MutableAppContext, Task};
|
||||
use participant::{LocalParticipant, ParticipantLocation, RemoteParticipant};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
pub enum Event {
|
||||
PeerChangedActiveProject,
|
||||
|
@ -18,9 +19,11 @@ pub struct Room {
|
|||
status: RoomStatus,
|
||||
local_participant: LocalParticipant,
|
||||
remote_participants: HashMap<PeerId, RemoteParticipant>,
|
||||
pending_user_ids: Vec<u64>,
|
||||
pending_users: Vec<Arc<User>>,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
_subscriptions: Vec<client::Subscription>,
|
||||
_load_pending_users: Option<Task<()>>,
|
||||
}
|
||||
|
||||
impl Entity for Room {
|
||||
|
@ -28,7 +31,44 @@ impl Entity for Room {
|
|||
}
|
||||
|
||||
impl Room {
|
||||
fn new(id: u64, client: Arc<Client>, cx: &mut ModelContext<Self>) -> Self {
|
||||
pub fn observe<F>(cx: &mut MutableAppContext, mut callback: F) -> gpui::Subscription
|
||||
where
|
||||
F: 'static + FnMut(Option<ModelHandle<Self>>, &mut MutableAppContext),
|
||||
{
|
||||
cx.observe_default_global::<Option<ModelHandle<Self>>, _>(move |cx| {
|
||||
let room = cx.global::<Option<ModelHandle<Self>>>().clone();
|
||||
callback(room, cx);
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_or_create(
|
||||
client: &Arc<Client>,
|
||||
user_store: &ModelHandle<UserStore>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
if let Some(room) = cx.global::<Option<ModelHandle<Self>>>() {
|
||||
Task::ready(Ok(room.clone()))
|
||||
} else {
|
||||
let client = client.clone();
|
||||
let user_store = user_store.clone();
|
||||
cx.spawn(|mut cx| async move {
|
||||
let room = cx.update(|cx| Room::create(client, user_store, cx)).await?;
|
||||
cx.update(|cx| cx.set_global(Some(room.clone())));
|
||||
Ok(room)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(cx: &mut MutableAppContext) {
|
||||
cx.set_global::<Option<ModelHandle<Self>>>(None);
|
||||
}
|
||||
|
||||
fn new(
|
||||
id: u64,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Self {
|
||||
let mut client_status = client.status();
|
||||
cx.spawn_weak(|this, mut cx| async move {
|
||||
let is_connected = client_status
|
||||
|
@ -51,32 +91,36 @@ impl Room {
|
|||
projects: Default::default(),
|
||||
},
|
||||
remote_participants: Default::default(),
|
||||
pending_user_ids: Default::default(),
|
||||
pending_users: Default::default(),
|
||||
_subscriptions: vec![client.add_message_handler(cx.handle(), Self::handle_room_updated)],
|
||||
_load_pending_users: None,
|
||||
client,
|
||||
user_store,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create(
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
cx.spawn(|mut cx| async move {
|
||||
let room = client.request(proto::CreateRoom {}).await?;
|
||||
Ok(cx.add_model(|cx| Self::new(room.id, client, cx)))
|
||||
Ok(cx.add_model(|cx| Self::new(room.id, client, user_store, cx)))
|
||||
})
|
||||
}
|
||||
|
||||
pub fn join(
|
||||
call: &Call,
|
||||
client: Arc<Client>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut MutableAppContext,
|
||||
) -> Task<Result<ModelHandle<Self>>> {
|
||||
let room_id = call.room_id;
|
||||
cx.spawn(|mut cx| async move {
|
||||
let response = client.request(proto::JoinRoom { id: room_id }).await?;
|
||||
let room_proto = response.room.ok_or_else(|| anyhow!("invalid room"))?;
|
||||
let room = cx.add_model(|cx| Self::new(room_id, client, cx));
|
||||
let room = cx.add_model(|cx| Self::new(room_id, client, user_store, cx));
|
||||
room.update(&mut cx, |room, cx| room.apply_room_update(room_proto, cx))?;
|
||||
Ok(room)
|
||||
})
|
||||
|
@ -98,8 +142,8 @@ impl Room {
|
|||
&self.remote_participants
|
||||
}
|
||||
|
||||
pub fn pending_user_ids(&self) -> &[u64] {
|
||||
&self.pending_user_ids
|
||||
pub fn pending_users(&self) -> &[Arc<User>] {
|
||||
&self.pending_users
|
||||
}
|
||||
|
||||
async fn handle_room_updated(
|
||||
|
@ -131,7 +175,19 @@ impl Room {
|
|||
);
|
||||
}
|
||||
}
|
||||
self.pending_user_ids = room.pending_user_ids;
|
||||
|
||||
let pending_users = self.user_store.update(cx, move |user_store, cx| {
|
||||
user_store.get_users(room.pending_user_ids, cx)
|
||||
});
|
||||
self._load_pending_users = Some(cx.spawn(|this, mut cx| async move {
|
||||
if let Some(pending_users) = pending_users.await.log_err() {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.pending_users = pending_users;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
}));
|
||||
|
||||
cx.notify();
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -21,10 +21,11 @@ use gpui::{
|
|||
geometry::vector::vec2f,
|
||||
impl_actions,
|
||||
platform::{WindowBounds, WindowOptions},
|
||||
AssetSource, AsyncAppContext, TitlebarOptions, ViewContext, WindowKind,
|
||||
AssetSource, AsyncAppContext, ModelHandle, TitlebarOptions, ViewContext, WindowKind,
|
||||
};
|
||||
use language::Rope;
|
||||
pub use lsp;
|
||||
use postage::watch;
|
||||
pub use project::{self, fs};
|
||||
use project_panel::ProjectPanel;
|
||||
use search::{BufferSearchBar, ProjectSearchBar};
|
||||
|
|
Loading…
Reference in a new issue