remote projects per user (#10594)

Release Notes:

- Made remote projects per-user instead of per-channel. If you'd like to
be part of the remote development alpha, please email hi@zed.dev.

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet <bennetbo@gmx.de>
Co-authored-by: Nate Butler <1714999+iamnbutler@users.noreply.github.com>
Co-authored-by: Nate Butler <iamnbutler@gmail.com>
This commit is contained in:
Conrad Irwin 2024-04-23 15:33:09 -06:00 committed by GitHub
parent 8ae4c3277f
commit e0c83a1d32
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 2807 additions and 1625 deletions

View file

@ -39,7 +39,6 @@ db.workspace = true
editor.workspace = true
emojis.workspace = true
extensions_ui.workspace = true
feature_flags.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true

View file

@ -305,10 +305,6 @@ impl ChannelView {
});
}
ChannelBufferEvent::BufferEdited => {
// Emit the edited event on the editor context so that other views can update it's state (e.g. markdown preview)
self.editor.update(cx, |_, cx| {
cx.emit(EditorEvent::Edited);
});
if self.editor.read(cx).is_focused(cx) {
self.acknowledge_buffer_version(cx);
} else {

View file

@ -1,20 +1,17 @@
mod channel_modal;
mod contact_finder;
mod dev_server_modal;
use self::channel_modal::ChannelModal;
use self::dev_server_modal::DevServerModal;
use crate::{
channel_view::ChannelView, chat_panel::ChatPanel, face_pile::FacePile,
CollaborationPanelSettings,
};
use call::ActiveCall;
use channel::{Channel, ChannelEvent, ChannelStore, RemoteProject};
use channel::{Channel, ChannelEvent, ChannelStore};
use client::{ChannelId, Client, Contact, ProjectId, User, UserStore};
use contact_finder::ContactFinder;
use db::kvp::KEY_VALUE_STORE;
use editor::{Editor, EditorElement, EditorStyle};
use feature_flags::{self, FeatureFlagAppExt};
use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{
actions, anchored, canvas, deferred, div, fill, list, point, prelude::*, px, AnyElement,
@ -27,7 +24,7 @@ use gpui::{
use menu::{Cancel, Confirm, SecondaryConfirm, SelectNext, SelectPrev};
use project::{Fs, Project};
use rpc::{
proto::{self, ChannelVisibility, DevServerStatus, PeerId},
proto::{self, ChannelVisibility, PeerId},
ErrorCode, ErrorExt,
};
use serde_derive::{Deserialize, Serialize};
@ -191,7 +188,6 @@ enum ListEntry {
id: ProjectId,
name: SharedString,
},
RemoteProject(channel::RemoteProject),
Contact {
contact: Arc<Contact>,
calling: bool,
@ -282,23 +278,10 @@ impl CollabPanel {
.push(cx.observe(&this.user_store, |this, _, cx| {
this.update_entries(true, cx)
}));
let mut has_opened = false;
this.subscriptions.push(cx.observe(
&this.channel_store,
move |this, channel_store, cx| {
if !has_opened {
if !channel_store
.read(cx)
.dev_servers_for_id(ChannelId(1))
.is_empty()
{
this.manage_remote_projects(ChannelId(1), cx);
has_opened = true;
}
}
this.subscriptions
.push(cx.observe(&this.channel_store, move |this, _, cx| {
this.update_entries(true, cx)
},
));
}));
this.subscriptions
.push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx)));
this.subscriptions.push(cx.subscribe(
@ -586,7 +569,6 @@ impl CollabPanel {
}
let hosted_projects = channel_store.projects_for_id(channel.id);
let remote_projects = channel_store.remote_projects_for_id(channel.id);
let has_children = channel_store
.channel_at_index(mat.candidate_id + 1)
.map_or(false, |next_channel| {
@ -624,12 +606,6 @@ impl CollabPanel {
for (name, id) in hosted_projects {
self.entries.push(ListEntry::HostedProject { id, name });
}
if cx.has_flag::<feature_flags::Remoting>() {
for remote_project in remote_projects {
self.entries.push(ListEntry::RemoteProject(remote_project));
}
}
}
}
@ -1089,59 +1065,6 @@ impl CollabPanel {
.tooltip(move |cx| Tooltip::text("Open Project", cx))
}
fn render_remote_project(
&self,
remote_project: &RemoteProject,
is_selected: bool,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let id = remote_project.id;
let name = remote_project.name.clone();
let maybe_project_id = remote_project.project_id;
let dev_server = self
.channel_store
.read(cx)
.find_dev_server_by_id(remote_project.dev_server_id);
let tooltip_text = SharedString::from(match dev_server {
Some(dev_server) => format!("Open Remote Project ({})", dev_server.name),
None => "Open Remote Project".to_string(),
});
let dev_server_is_online = dev_server.map(|s| s.status) == Some(DevServerStatus::Online);
let dev_server_text_color = if dev_server_is_online {
Color::Default
} else {
Color::Disabled
};
ListItem::new(ElementId::NamedInteger(
"remote-project".into(),
id.0 as usize,
))
.indent_level(2)
.indent_step_size(px(20.))
.selected(is_selected)
.on_click(cx.listener(move |this, _, cx| {
//TODO display error message if dev server is offline
if dev_server_is_online {
if let Some(project_id) = maybe_project_id {
this.join_remote_project(project_id, cx);
}
}
}))
.start_slot(
h_flex()
.relative()
.gap_1()
.child(IconButton::new(0, IconName::FileTree).icon_color(dev_server_text_color)),
)
.child(Label::new(name.clone()).color(dev_server_text_color))
.tooltip(move |cx| Tooltip::text(tooltip_text.clone(), cx))
}
fn has_subchannels(&self, ix: usize) -> bool {
self.entries.get(ix).map_or(false, |entry| {
if let ListEntry::Channel { has_children, .. } = entry {
@ -1343,24 +1266,11 @@ impl CollabPanel {
}
if self.channel_store.read(cx).is_root_channel(channel_id) {
context_menu = context_menu
.separator()
.entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_members(channel_id, cx)
}),
)
.when(cx.has_flag::<feature_flags::Remoting>(), |context_menu| {
context_menu.entry(
"Manage Remote Projects",
None,
cx.handler_for(&this, move |this, cx| {
this.manage_remote_projects(channel_id, cx)
}),
)
})
context_menu = context_menu.separator().entry(
"Manage Members",
None,
cx.handler_for(&this, move |this, cx| this.manage_members(channel_id, cx)),
)
} else {
context_menu = context_menu.entry(
"Move this channel",
@ -1624,12 +1534,6 @@ impl CollabPanel {
} => {
// todo()
}
ListEntry::RemoteProject(project) => {
if let Some(project_id) = project.project_id {
self.join_remote_project(project_id, cx)
}
}
ListEntry::OutgoingRequest(_) => {}
ListEntry::ChannelEditor { .. } => {}
}
@ -1801,18 +1705,6 @@ impl CollabPanel {
self.show_channel_modal(channel_id, channel_modal::Mode::ManageMembers, cx);
}
fn manage_remote_projects(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let channel_store = self.channel_store.clone();
let Some(workspace) = self.workspace.upgrade() else {
return;
};
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| {
DevServerModal::new(channel_store.clone(), channel_id, cx)
});
});
}
fn remove_selected_channel(&mut self, _: &Remove, cx: &mut ViewContext<Self>) {
if let Some(channel) = self.selected_channel() {
self.remove_channel(channel.id, cx)
@ -2113,18 +2005,6 @@ impl CollabPanel {
.detach_and_prompt_err("Failed to join channel", cx, |_, _| None)
}
fn join_remote_project(&mut self, project_id: ProjectId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let app_state = workspace.read(cx).app_state().clone();
workspace::join_remote_project(project_id, app_state, cx).detach_and_prompt_err(
"Failed to join project",
cx,
|_, _| None,
)
}
fn join_channel_chat(&mut self, channel_id: ChannelId, cx: &mut ViewContext<Self>) {
let Some(workspace) = self.workspace.upgrade() else {
return;
@ -2260,9 +2140,6 @@ impl CollabPanel {
ListEntry::HostedProject { id, name } => self
.render_channel_project(*id, name, is_selected, cx)
.into_any_element(),
ListEntry::RemoteProject(remote_project) => self
.render_remote_project(remote_project, is_selected, cx)
.into_any_element(),
}
}
@ -3005,11 +2882,6 @@ impl PartialEq for ListEntry {
return id == other_id;
}
}
ListEntry::RemoteProject(project) => {
if let ListEntry::RemoteProject(other) = other {
return project.id == other.id;
}
}
ListEntry::ChannelNotes { channel_id } => {
if let ListEntry::ChannelNotes {
channel_id: other_id,

View file

@ -1,622 +0,0 @@
use channel::{ChannelStore, DevServer, RemoteProject};
use client::{ChannelId, DevServerId, RemoteProjectId};
use editor::Editor;
use gpui::{
AppContext, ClipboardItem, DismissEvent, EventEmitter, FocusHandle, FocusableView, Model,
ScrollHandle, Task, View, ViewContext,
};
use rpc::proto::{self, CreateDevServerResponse, DevServerStatus};
use ui::{prelude::*, Indicator, List, ListHeader, ModalContent, ModalHeader, Tooltip};
use util::ResultExt;
use workspace::ModalView;
pub struct DevServerModal {
mode: Mode,
focus_handle: FocusHandle,
scroll_handle: ScrollHandle,
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
remote_project_name_editor: View<Editor>,
remote_project_path_editor: View<Editor>,
dev_server_name_editor: View<Editor>,
_subscriptions: [gpui::Subscription; 2],
}
#[derive(Default)]
struct CreateDevServer {
creating: Option<Task<()>>,
dev_server: Option<CreateDevServerResponse>,
}
struct CreateRemoteProject {
dev_server_id: DevServerId,
creating: Option<Task<()>>,
remote_project: Option<proto::RemoteProject>,
}
enum Mode {
Default,
CreateRemoteProject(CreateRemoteProject),
CreateDevServer(CreateDevServer),
}
impl DevServerModal {
pub fn new(
channel_store: Model<ChannelStore>,
channel_id: ChannelId,
cx: &mut ViewContext<Self>,
) -> Self {
let name_editor = cx.new_view(|cx| Editor::single_line(cx));
let path_editor = cx.new_view(|cx| Editor::single_line(cx));
let dev_server_name_editor = cx.new_view(|cx| {
let mut editor = Editor::single_line(cx);
editor.set_placeholder_text("Dev server name", cx);
editor
});
let focus_handle = cx.focus_handle();
let subscriptions = [
cx.observe(&channel_store, |_, _, cx| {
cx.notify();
}),
cx.on_focus_out(&focus_handle, |_, _cx| { /* cx.emit(DismissEvent) */ }),
];
Self {
mode: Mode::Default,
focus_handle,
scroll_handle: ScrollHandle::new(),
channel_store,
channel_id,
remote_project_name_editor: name_editor,
remote_project_path_editor: path_editor,
dev_server_name_editor,
_subscriptions: subscriptions,
}
}
pub fn create_remote_project(
&mut self,
dev_server_id: DevServerId,
cx: &mut ViewContext<Self>,
) {
let channel_id = self.channel_id;
let name = self
.remote_project_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
let path = self
.remote_project_path_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
if path == "" {
return;
}
let create = self.channel_store.update(cx, |store, cx| {
store.create_remote_project(channel_id, dev_server_id, name, path, cx)
});
let task = cx.spawn(|this, mut cx| async move {
let result = create.await;
if let Err(e) = &result {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create project",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
}
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: result.ok().and_then(|r| r.remote_project),
});
})
.log_err();
});
self.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: Some(task),
remote_project: None,
});
}
pub fn create_dev_server(&mut self, cx: &mut ViewContext<Self>) {
let name = self
.dev_server_name_editor
.read(cx)
.text(cx)
.trim()
.to_string();
if name == "" {
return;
}
let dev_server = self.channel_store.update(cx, |store, cx| {
store.create_dev_server(self.channel_id, name.clone(), cx)
});
let task = cx.spawn(|this, mut cx| async move {
match dev_server.await {
Ok(dev_server) => {
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(CreateDevServer {
creating: None,
dev_server: Some(dev_server),
});
})
.log_err();
}
Err(e) => {
cx.prompt(
gpui::PromptLevel::Critical,
"Failed to create server",
Some(&format!("{:?}. Please try again.", e)),
&["Ok"],
)
.await
.log_err();
this.update(&mut cx, |this, _| {
this.mode = Mode::CreateDevServer(Default::default());
})
.log_err();
}
}
});
self.mode = Mode::CreateDevServer(CreateDevServer {
creating: Some(task),
dev_server: None,
});
cx.notify()
}
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
match self.mode {
Mode::Default => cx.emit(DismissEvent),
Mode::CreateRemoteProject(_) | Mode::CreateDevServer(_) => {
self.mode = Mode::Default;
cx.notify();
}
}
}
fn render_dev_server(
&mut self,
dev_server: &DevServer,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_server_id = dev_server.id;
let status = dev_server.status;
v_flex()
.w_full()
.child(
h_flex()
.group("dev-server")
.justify_between()
.child(
h_flex()
.gap_2()
.child(
div()
.id(("status", dev_server.id.0))
.relative()
.child(Icon::new(IconName::Server).size(IconSize::Small))
.child(
div().absolute().bottom_0().left(rems_from_px(8.0)).child(
Indicator::dot().color(match status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
),
)
.tooltip(move |cx| {
Tooltip::text(
match status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server.name.clone())
.child(
h_flex()
.visible_on_hover("dev-server")
.gap_1()
.child(
IconButton::new("edit-dev-server", IconName::Pencil)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Edit dev server", cx)
}),
)
.child(
IconButton::new("remove-dev-server", IconName::Trash)
.disabled(true) //TODO implement this on the collab side
.tooltip(|cx| {
Tooltip::text("Coming Soon - Remove dev server", cx)
}),
),
),
)
.child(
h_flex().gap_1().child(
IconButton::new("add-remote-project", IconName::Plus)
.tooltip(|cx| Tooltip::text("Add a remote project", cx))
.on_click(cx.listener(move |this, _, cx| {
this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating: None,
remote_project: None,
});
cx.notify();
})),
),
),
)
.child(
v_flex()
.w_full()
.bg(cx.theme().colors().title_bar_background)
.border()
.border_color(cx.theme().colors().border_variant)
.rounded_md()
.my_1()
.py_0p5()
.px_3()
.child(
List::new().empty_message("No projects.").children(
channel_store
.remote_projects_for_id(dev_server.channel_id)
.iter()
.filter_map(|remote_project| {
if remote_project.dev_server_id == dev_server.id {
Some(self.render_remote_project(remote_project, cx))
} else {
None
}
}),
),
),
)
// .child(div().ml_8().child(
// Button::new(("add-project", dev_server_id.0), "Add Project").on_click(cx.listener(
// move |this, _, cx| {
// this.mode = Mode::CreateRemoteProject(CreateRemoteProject {
// dev_server_id,
// creating: None,
// remote_project: None,
// });
// cx.notify();
// },
// )),
// ))
}
fn render_remote_project(
&mut self,
project: &RemoteProject,
_: &mut ViewContext<Self>,
) -> impl IntoElement {
h_flex()
.gap_2()
.child(Icon::new(IconName::FileTree))
.child(Label::new(project.name.clone()))
.child(Label::new(format!("({})", project.path.clone())).color(Color::Muted))
}
fn render_create_dev_server(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateDevServer(CreateDevServer {
creating,
dev_server,
}) = &self.mode
else {
unreachable!()
};
self.dev_server_name_editor.update(cx, |editor, _| {
editor.set_read_only(creating.is_some() || dev_server.is_some())
});
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
v_flex().py_0p5().px_1().child(
h_flex()
.px_1()
.py_0p5()
.child(
IconButton::new("back", IconName::ArrowLeft)
.style(ButtonStyle::Transparent)
.on_click(cx.listener(|this, _: &gpui::ClickEvent, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
.child(Headline::new("Register dev server")),
),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.dev_server_name_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && dev_server.is_none(), |div| {
div.child(
Button::new("create-dev-server", "Create").on_click(cx.listener(
move |this, _, cx| {
this.create_dev_server(cx);
},
)),
)
})
.when(creating.is_some() && dev_server.is_none(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(dev_server.clone(), |div, dev_server| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_dev_server_by_id(DevServerId(dev_server.dev_server_id))
.map(|server| server.status)
.unwrap_or(DevServerStatus::Offline);
let instructions = SharedString::from(format!(
"zed --dev-server-token {}",
dev_server.access_token
));
div.child(
v_flex()
.ml_8()
.gap_2()
.child(Label::new(format!(
"Please log into `{}` and run:",
dev_server.name
)))
.child(instructions.clone())
.child(
IconButton::new("copy-access-token", IconName::Copy)
.on_click(cx.listener(move |_, _, cx| {
cx.write_to_clipboard(ClipboardItem::new(
instructions.to_string(),
))
}))
.icon_size(IconSize::Small)
.tooltip(|cx| Tooltip::text("Copy access token", cx)),
)
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for connection..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Connection established! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
fn render_default(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let channel_store = self.channel_store.read(cx);
let dev_servers = channel_store.dev_servers_for_id(self.channel_id);
// let dev_servers = Vec::new();
v_flex()
.id("scroll-container")
.h_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle)
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Remote Projects").size(HeadlineSize::Small)),
)
.child(
ModalContent::new().child(
List::new()
.empty_message("No dev servers registered.")
.header(Some(
ListHeader::new("Dev Servers").end_slot(
Button::new("register-dev-server-button", "New Server")
.icon(IconName::Plus)
.icon_position(IconPosition::Start)
.tooltip(|cx| Tooltip::text("Register a new dev server", cx))
.on_click(cx.listener(|this, _, cx| {
this.mode = Mode::CreateDevServer(Default::default());
this.dev_server_name_editor
.read(cx)
.focus_handle(cx)
.focus(cx);
cx.notify();
})),
),
))
.children(dev_servers.iter().map(|dev_server| {
self.render_dev_server(dev_server, cx).into_any_element()
})),
),
)
}
fn render_create_project(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Mode::CreateRemoteProject(CreateRemoteProject {
dev_server_id,
creating,
remote_project,
}) = &self.mode
else {
unreachable!()
};
let channel_store = self.channel_store.read(cx);
let (dev_server_name, dev_server_status) = channel_store
.find_dev_server_by_id(*dev_server_id)
.map(|server| (server.name.clone(), server.status))
.unwrap_or((SharedString::from(""), DevServerStatus::Offline));
v_flex()
.px_1()
.pt_0p5()
.gap_px()
.child(
ModalHeader::new("Manage Remote Project")
.child(Headline::new("Manage Remote Projects")),
)
.child(
h_flex()
.py_0p5()
.px_1()
.child(div().px_1().py_0p5().child(
IconButton::new("back", IconName::ArrowLeft).on_click(cx.listener(
|this, _, cx| {
this.mode = Mode::Default;
cx.notify()
},
)),
))
.child("Add Project..."),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child(
div()
.id(("status", dev_server_id.0))
.relative()
.child(Icon::new(IconName::Server))
.child(div().absolute().bottom_0().left(rems_from_px(12.0)).child(
Indicator::dot().color(match dev_server_status {
DevServerStatus::Online => Color::Created,
DevServerStatus::Offline => Color::Deleted,
}),
))
.tooltip(move |cx| {
Tooltip::text(
match dev_server_status {
DevServerStatus::Online => "Online",
DevServerStatus::Offline => "Offline",
},
cx,
)
}),
)
.child(dev_server_name.clone()),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Name")
.child(self.remote_project_name_editor.clone())
.on_action(cx.listener(|this, _: &menu::Confirm, cx| {
cx.focus_view(&this.remote_project_path_editor)
})),
)
.child(
h_flex()
.ml_5()
.gap_2()
.child("Path")
.child(self.remote_project_path_editor.clone())
.on_action(
cx.listener(|this, _: &menu::Confirm, cx| this.create_dev_server(cx)),
)
.when(creating.is_none() && remote_project.is_none(), |div| {
div.child(Button::new("create-remote-server", "Create").on_click({
let dev_server_id = *dev_server_id;
cx.listener(move |this, _, cx| {
this.create_remote_project(dev_server_id, cx)
})
}))
})
.when(creating.is_some(), |div| {
div.child(Button::new("create-dev-server", "Creating...").disabled(true))
}),
)
.when_some(remote_project.clone(), |div, remote_project| {
let channel_store = self.channel_store.read(cx);
let status = channel_store
.find_remote_project_by_id(RemoteProjectId(remote_project.id))
.map(|project| {
if project.project_id.is_some() {
DevServerStatus::Online
} else {
DevServerStatus::Offline
}
})
.unwrap_or(DevServerStatus::Offline);
div.child(
v_flex()
.ml_5()
.ml_8()
.gap_2()
.when(status == DevServerStatus::Offline, |this| {
this.child(Label::new("Waiting for project..."))
})
.when(status == DevServerStatus::Online, |this| {
this.child(Label::new("Project online! 🎊")).child(
Button::new("done", "Done").on_click(cx.listener(|this, _, cx| {
this.mode = Mode::Default;
cx.notify();
})),
)
}),
)
})
}
}
impl ModalView for DevServerModal {}
impl FocusableView for DevServerModal {
fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
self.focus_handle.clone()
}
}
impl EventEmitter<DismissEvent> for DevServerModal {}
impl Render for DevServerModal {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
div()
.track_focus(&self.focus_handle)
.elevation_3(cx)
.key_context("DevServerModal")
.on_action(cx.listener(Self::cancel))
.pb_4()
.w(rems(34.))
.min_h(rems(20.))
.max_h(rems(40.))
.child(match &self.mode {
Mode::Default => self.render_default(cx).into_any_element(),
Mode::CreateRemoteProject(_) => self.render_create_project(cx).into_any_element(),
Mode::CreateDevServer(_) => self.render_create_dev_server(cx).into_any_element(),
})
}
}

View file

@ -171,44 +171,48 @@ impl Render for CollabTitlebarItem {
let room = room.read(cx);
let project = self.project.read(cx);
let is_local = project.is_local();
let is_shared = is_local && project.is_shared();
let is_remote_project = project.remote_project_id().is_some();
let is_shared = (is_local || is_remote_project) && project.is_shared();
let is_muted = room.is_muted();
let is_deafened = room.is_deafened().unwrap_or(false);
let is_screen_sharing = room.is_screen_sharing();
let can_use_microphone = room.can_use_microphone();
let can_share_projects = room.can_share_projects();
this.when(is_local && can_share_projects, |this| {
this.child(
Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
)
.tooltip(move |cx| {
Tooltip::text(
if is_shared {
"Stop sharing project with call participants"
} else {
"Share project with call participants"
},
cx,
this.when(
(is_local || is_remote_project) && can_share_projects,
|this| {
this.child(
Button::new(
"toggle_sharing",
if is_shared { "Unshare" } else { "Share" },
)
})
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
.label_size(LabelSize::Small)
.on_click(cx.listener(
move |this, _, cx| {
if is_shared {
this.unshare_project(&Default::default(), cx);
} else {
this.share_project(&Default::default(), cx);
}
},
)),
)
})
.tooltip(move |cx| {
Tooltip::text(
if is_shared {
"Stop sharing project with call participants"
} else {
"Share project with call participants"
},
cx,
)
})
.style(ButtonStyle::Subtle)
.selected_style(ButtonStyle::Tinted(TintColor::Accent))
.selected(is_shared)
.label_size(LabelSize::Small)
.on_click(cx.listener(
move |this, _, cx| {
if is_shared {
this.unshare_project(&Default::default(), cx);
} else {
this.share_project(&Default::default(), cx);
}
},
)),
)
},
)
.child(
div()
.child(
@ -406,7 +410,7 @@ impl CollabTitlebarItem {
)
}
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl Element {
pub fn render_project_name(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let name = {
let mut names = self.project.read(cx).visible_worktrees(cx).map(|worktree| {
let worktree = worktree.read(cx);
@ -423,15 +427,26 @@ impl CollabTitlebarItem {
};
let workspace = self.workspace.clone();
popover_menu("project_name_trigger")
.trigger(
Button::new("project_name_trigger", name)
.when(!is_project_selected, |b| b.color(Color::Muted))
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| Tooltip::text("Recent Projects", cx)),
)
.menu(move |cx| Some(Self::render_project_popover(workspace.clone(), cx)))
Button::new("project_name_trigger", name)
.when(!is_project_selected, |b| b.color(Color::Muted))
.style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.tooltip(move |cx| {
Tooltip::for_action(
"Recent Projects",
&recent_projects::OpenRecent {
create_new_window: false,
},
cx,
)
})
.on_click(cx.listener(move |_, _, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
RecentProjects::open(workspace, false, cx);
})
}
}))
}
pub fn render_project_branch(&self, cx: &mut ViewContext<Self>) -> Option<impl Element> {
@ -607,17 +622,6 @@ impl CollabTitlebarItem {
Some(view)
}
pub fn render_project_popover(
workspace: WeakView<Workspace>,
cx: &mut WindowContext<'_>,
) -> View<RecentProjects> {
let view = RecentProjects::open_popover(workspace, cx);
let focus_handle = view.focus_handle(cx);
cx.focus(&focus_handle);
view
}
fn render_connection_status(
&self,
status: &client::Status,