diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs index 2f742814a8..69cfb7102b 100644 --- a/crates/cli/src/main.rs +++ b/crates/cli/src/main.rs @@ -182,6 +182,7 @@ impl Bundle { kCFStringEncodingUTF8, ptr::null(), )); + // equivalent to: open zed-cli:... -a /Applications/Zed\ Preview.app let urls_to_open = CFArray::from_copyable(&[url_to_open.as_concrete_TypeRef()]); LSOpenFromURLSpec( &LSLaunchURLSpec { diff --git a/crates/collab_ui/src/collab_panel.rs b/crates/collab_ui/src/collab_panel.rs index 951c8bf70c..3d66e8450a 100644 --- a/crates/collab_ui/src/collab_panel.rs +++ b/crates/collab_ui/src/collab_panel.rs @@ -1969,18 +1969,21 @@ impl CollabPanel { let style = collab_theme.channel_name.inactive_state(); Flex::row() .with_child( - Label::new(channel.name.clone(), style.text.clone()) - .contained() - .with_style(style.container) - .aligned() - .left() - .with_tooltip::( - ix, - "Join channel", - None, - theme.tooltip.clone(), - cx, - ), + Label::new( + channel.name.clone().to_owned() + channel_id.to_string().as_str(), + style.text.clone(), + ) + .contained() + .with_style(style.container) + .aligned() + .left() + .with_tooltip::( + ix, + "Join channel", + None, + theme.tooltip.clone(), + cx, + ), ) .with_children({ let participants = @@ -3187,49 +3190,19 @@ impl CollabPanel { } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { - let workspace = self.workspace.clone(); - let window = cx.window(); - let active_call = ActiveCall::global(cx); - cx.spawn(|_, mut cx| async move { - if active_call.read_with(&mut cx, |active_call, cx| { - if let Some(room) = active_call.room() { - let room = room.read(cx); - room.is_sharing_project() && room.remote_participants().len() > 0 - } else { - false - } - }) { - let answer = window.prompt( - PromptLevel::Warning, - "Leaving this call will unshare your current project.\nDo you want to switch channels?", - &["Yes, Join Channel", "Cancel"], - &mut cx, - ); - - if let Some(mut answer) = answer { - if answer.next().await == Some(1) { - return anyhow::Ok(()); - } - } - } - - let room = active_call - .update(&mut cx, |call, cx| call.join_channel(channel_id, cx)) - .await?; - - let task = room.update(&mut cx, |room, cx| { - let workspace = workspace.upgrade(cx)?; - let (project, host) = room.most_active_project()?; - let app_state = workspace.read(cx).app_state().clone(); - Some(workspace::join_remote_project(project, host, app_state, cx)) - }); - if let Some(task) = task { - task.await?; - } - - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let Some(workspace) = self.workspace.upgrade(cx) else { + return; + }; + let Some(handle) = cx.window().downcast::() else { + return; + }; + workspace::join_channel( + channel_id, + workspace.read(cx).app_state().clone(), + Some(handle), + cx, + ) + .detach_and_log_err(cx) } fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 89d42ffba6..761b17e6af 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -17,15 +17,14 @@ lazy_static! { _ => panic!("invalid release channel {}", *RELEASE_CHANNEL_NAME), }; - static ref URL_SCHEME: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + pub static ref URL_SCHEME_PREFIX: String = match RELEASE_CHANNEL_NAME.as_str() { "dev" => "zed-dev:/", "preview" => "zed-preview:/", "stable" => "zed:/", - // NOTE: this must be kept in sync with ./script/bundle and https://zed.dev. + // NOTE: this must be kept in sync with osx_url_schemes in Cargo.toml and with https://zed.dev. _ => unreachable!(), - }) - .unwrap(); - static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { + }.to_string(); + pub static ref LINK_PREFIX: Url = Url::parse(match RELEASE_CHANNEL_NAME.as_str() { "dev" => "http://localhost:3000/dev/", "preview" => "https://zed.dev/preview/", "stable" => "https://zed.dev/", @@ -59,12 +58,4 @@ impl ReleaseChannel { ReleaseChannel::Stable => "stable", } } - - pub fn url_scheme(&self) -> &'static Url { - &URL_SCHEME - } - - pub fn link_prefix(&self) -> &'static Url { - &LINK_PREFIX - } } diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index f7bb409229..5ec847b28b 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -4154,6 +4154,88 @@ pub async fn last_opened_workspace_paths() -> Option { DB.last_workspace().await.log_err().flatten() } +pub fn join_channel( + channel_id: u64, + app_state: Arc, + requesting_window: Option>, + cx: &mut AppContext, +) -> Task> { + let active_call = ActiveCall::global(cx); + cx.spawn(|mut cx| async move { + let should_prompt = active_call.read_with(&mut cx, |active_call, cx| { + let Some(room) = active_call.room().map( |room| room.read(cx) ) else { + return false + }; + + room.is_sharing_project() && room.remote_participants().len() > 0 && + room.channel_id() != Some(channel_id) + }); + + if should_prompt { + if let Some(workspace) = requesting_window { + if let Some(window) = workspace.update(&mut cx, |cx| { + cx.window() + }) { + let answer = window.prompt( + PromptLevel::Warning, + "Leaving this call will unshare your current project.\nDo you want to switch channels?", + &["Yes, Join Channel", "Cancel"], + &mut cx, + ); + + if let Some(mut answer) = answer { + if answer.next().await == Some(1) { + return Ok(()); + } + } + } + } + } + + let room = active_call.update(&mut cx, |active_call, cx| { + active_call.join_channel(channel_id, cx) + }).await?; + + let task = room.update(&mut cx, |room, cx| { + if let Some((project, host)) = room.most_active_project() { + return Some(join_remote_project(project, host, app_state.clone(), cx)) + } + + None + }); + if let Some(task) = task { + task.await?; + return anyhow::Ok(()); + } + + if requesting_window.is_some() { + return anyhow::Ok(()); + } + + // find an existing workspace to focus and show call controls + for window in cx.windows() { + let found = window.update(&mut cx, |cx| { + let is_workspace = cx.root_view().clone().downcast::().is_some(); + if is_workspace { + cx.activate_window(); + } + is_workspace + }); + + if found.unwrap_or(false) { + return anyhow::Ok(()) + } + } + + // no open workspaces + cx.update(|cx| { + Workspace::new_local(vec![], app_state.clone(), requesting_window, cx) + }).await; + + return anyhow::Ok(()); + }) +} + #[allow(clippy::type_complexity)] pub fn open_paths( abs_paths: &[PathBuf], diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index d6f3be2b46..c491d406af 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -45,7 +45,7 @@ use std::{ }; use sum_tree::Bias; use util::{ - channel::ReleaseChannel, + channel::{ReleaseChannel, URL_SCHEME_PREFIX}, http::{self, HttpClient}, paths::PathLikeWithPosition, }; @@ -61,6 +61,10 @@ use zed::{ only_instance::{ensure_only_instance, IsOnlyInstance}, }; +use crate::open_url::{OpenListener, OpenRequest}; + +mod open_url; + fn main() { let http = http::client(); init_paths(); @@ -92,29 +96,20 @@ fn main() { }) }; - let (cli_connections_tx, mut cli_connections_rx) = mpsc::unbounded(); - let cli_connections_tx = Arc::new(cli_connections_tx); - let (open_paths_tx, mut open_paths_rx) = mpsc::unbounded(); - let open_paths_tx = Arc::new(open_paths_tx); - let urls_callback_triggered = Arc::new(AtomicBool::new(false)); - - let callback_cli_connections_tx = Arc::clone(&cli_connections_tx); - let callback_open_paths_tx = Arc::clone(&open_paths_tx); - let callback_urls_callback_triggered = Arc::clone(&urls_callback_triggered); - app.on_open_urls(move |urls, _| { - callback_urls_callback_triggered.store(true, Ordering::Release); - open_urls(urls, &callback_cli_connections_tx, &callback_open_paths_tx); - }) - .on_reopen(move |cx| { - if cx.has_global::>() { - if let Some(app_state) = cx.global::>().upgrade() { - workspace::open_new(&app_state, cx, |workspace, cx| { - Editor::new_file(workspace, &Default::default(), cx) - }) - .detach(); + let (listener, mut open_rx) = OpenListener::new(); + let listener = Arc::new(listener); + let callback_listener = listener.clone(); + app.on_open_urls(move |urls, _| callback_listener.open_urls(urls)) + .on_reopen(move |cx| { + if cx.has_global::>() { + if let Some(app_state) = cx.global::>().upgrade() { + workspace::open_new(&app_state, cx, |workspace, cx| { + Editor::new_file(workspace, &Default::default(), cx) + }) + .detach(); + } } - } - }); + }); app.run(move |cx| { cx.set_global(*RELEASE_CHANNEL); @@ -226,41 +221,52 @@ fn main() { // TODO Development mode that forces the CLI mode usually runs Zed binary as is instead // of an *app, hence gets no specific callbacks run. Emulate them here, if needed. if std::env::var(FORCE_CLI_MODE_ENV_VAR_NAME).ok().is_some() - && !urls_callback_triggered.load(Ordering::Acquire) + && !listener.triggered.load(Ordering::Acquire) { - open_urls(collect_url_args(), &cli_connections_tx, &open_paths_tx) + listener.open_urls(collect_url_args()) } - if let Ok(Some(connection)) = cli_connections_rx.try_next() { - cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) - .detach(); - } else if let Ok(Some(paths)) = open_paths_rx.try_next() { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); - } else { - cx.spawn({ - let app_state = app_state.clone(); - |cx| async move { restore_or_create_workspace(&app_state, cx).await } - }) - .detach() - } - - cx.spawn(|cx| { - let app_state = app_state.clone(); - async move { - while let Some(connection) = cli_connections_rx.next().await { - handle_cli_connection(connection, app_state.clone(), cx.clone()).await; - } + match open_rx.try_next() { + Ok(Some(OpenRequest::Paths { paths })) => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); } - }) - .detach(); + Ok(Some(OpenRequest::CliConnection { connection })) => { + cx.spawn(|cx| handle_cli_connection(connection, app_state.clone(), cx)) + .detach(); + } + Ok(Some(OpenRequest::JoinChannel { channel_id })) => cx + .update(|cx| workspace::join_channel(channel_id, app_state.clone(), None, cx)) + .detach(), + Ok(None) | Err(_) => cx + .spawn({ + let app_state = app_state.clone(); + |cx| async move { restore_or_create_workspace(&app_state, cx).await } + }) + .detach(), + } cx.spawn(|mut cx| { let app_state = app_state.clone(); async move { - while let Some(paths) = open_paths_rx.next().await { - cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) - .detach(); + while let Some(request) = open_rx.next().await { + match request { + OpenRequest::Paths { paths } => { + cx.update(|cx| workspace::open_paths(&paths, &app_state, None, cx)) + .detach(); + } + OpenRequest::CliConnection { connection } => { + cx.spawn(|cx| { + handle_cli_connection(connection, app_state.clone(), cx) + }) + .detach(); + } + OpenRequest::JoinChannel { channel_id } => cx + .update(|cx| { + workspace::join_channel(channel_id, app_state.clone(), None, cx) + }) + .detach(), + } } } }) @@ -297,37 +303,6 @@ async fn installation_id() -> Result { } } -fn open_urls( - urls: Vec, - cli_connections_tx: &mpsc::UnboundedSender<( - mpsc::Receiver, - IpcSender, - )>, - open_paths_tx: &mpsc::UnboundedSender>, -) { - if let Some(server_name) = urls.first().and_then(|url| url.strip_prefix("zed-cli://")) { - if let Some(cli_connection) = connect_to_cli(server_name).log_err() { - cli_connections_tx - .unbounded_send(cli_connection) - .map_err(|_| anyhow!("no listener for cli connections")) - .log_err(); - }; - } else { - let paths: Vec<_> = urls - .iter() - .flat_map(|url| url.strip_prefix("file://")) - .map(|url| { - let decoded = urlencoding::decode_binary(url.as_bytes()); - PathBuf::from(OsStr::from_bytes(decoded.as_ref())) - }) - .collect(); - open_paths_tx - .unbounded_send(paths) - .map_err(|_| anyhow!("no listener for open urls requests")) - .log_err(); - } -} - async fn restore_or_create_workspace(app_state: &Arc, mut cx: AsyncAppContext) { if let Some(location) = workspace::last_opened_workspace_paths().await { cx.update(|cx| workspace::open_paths(location.paths().as_ref(), app_state, None, cx)) diff --git a/crates/zed/src/open_url.rs b/crates/zed/src/open_url.rs new file mode 100644 index 0000000000..f421633d5b --- /dev/null +++ b/crates/zed/src/open_url.rs @@ -0,0 +1,101 @@ +use anyhow::anyhow; +use cli::{ipc::IpcSender, CliRequest, CliResponse}; +use futures::channel::mpsc; +use futures::channel::mpsc::{UnboundedReceiver, UnboundedSender}; +use std::ffi::OsStr; +use std::os::unix::prelude::OsStrExt; +use std::sync::atomic::Ordering; +use std::{path::PathBuf, sync::atomic::AtomicBool}; +use util::channel::URL_SCHEME_PREFIX; +use util::ResultExt; + +use crate::{connect_to_cli, handle_cli_connection}; + +pub enum OpenRequest { + Paths { + paths: Vec, + }, + CliConnection { + connection: (mpsc::Receiver, IpcSender), + }, + JoinChannel { + channel_id: u64, + }, +} + +pub struct OpenListener { + tx: UnboundedSender, + pub triggered: AtomicBool, +} + +impl OpenListener { + pub fn new() -> (Self, UnboundedReceiver) { + let (tx, rx) = mpsc::unbounded(); + ( + OpenListener { + tx, + triggered: AtomicBool::new(false), + }, + rx, + ) + } + + pub fn open_urls(&self, urls: Vec) { + self.triggered.store(true, Ordering::Release); + dbg!(&urls); + let request = if let Some(server_name) = + urls.first().and_then(|url| url.strip_prefix("zed-cli://")) + { + self.handle_cli_connection(server_name) + } else if let Some(request_path) = urls + .first() + .and_then(|url| url.strip_prefix(URL_SCHEME_PREFIX.as_str())) + { + self.handle_zed_url_scheme(request_path) + } else { + self.handle_file_urls(urls) + }; + + if let Some(request) = request { + self.tx + .unbounded_send(request) + .map_err(|_| anyhow!("no listener for open requests")) + .log_err(); + } + } + + fn handle_cli_connection(&self, server_name: &str) -> Option { + if let Some(connection) = connect_to_cli(server_name).log_err() { + return Some(OpenRequest::CliConnection { connection }); + } + + None + } + + fn handle_zed_url_scheme(&self, request_path: &str) -> Option { + let mut parts = request_path.split("/"); + if parts.next() == Some("channel") { + if let Some(slug) = parts.next() { + if let Some(id_str) = slug.split("-").last() { + if let Ok(channel_id) = id_str.parse::() { + return Some(OpenRequest::JoinChannel { channel_id }); + } + } + } + } + None + } + + fn handle_file_urls(&self, urls: Vec) -> Option { + let paths: Vec<_> = urls + .iter() + .flat_map(|url| url.strip_prefix("file://")) + .map(|url| { + let decoded = urlencoding::decode_binary(url.as_bytes()); + PathBuf::from(OsStr::from_bytes(decoded.as_ref())) + }) + .collect(); + + Some(OpenRequest::Paths { paths }) + } +}